Study Programming!

保守的な金融系SIerからIT系ベンチャーに転職して1年目、カルチャーショックを受ける毎日です

オブジェクト指向の目的

オブジェクト指向ってよくわかんない。

継承、派生、カプセル化、クラス、インスタンス、色々技術があるけど結局何が目的なのかわかんない。。オブジェクト指向っぽいプログラムはクラスが分かれててあちこちに飛ぶし、場合によってはプロパティの宣言だけしかしてないクラスなんてあって、何のためなのか理解不能。

そんなことを思っている人向けの記事です。
自分が昔働いていた会社は金融系のシステム会社でした。90年代のスパゲッティコードををメンテするような仕事で、システムによってはドキュメントが電子化されていない、なんていうところもあった。転職して新興のベンチャー企業に入った。そこでオブジェクト指向について一から教えてくれた先輩には感謝しても感謝しきれないくらいだ。

そこで、自分なりにオブジェクト指向について整理するためにまとめてみる。
すごいざっくばらんに書いているので、色々抜け落ちていますが、とりあえず。

参考書籍等は以下の通り。他にもいくつか見てみたけど以下の3つが一番良かった。

  • 動画:Microsoft Virtual Academy の Testing with Visual Studio
  • 本:リーダブルコード
  • 本:チーム開発の教科書

オブジェクト指向って結局何が目的なのよ

まず、僕なりの理解を述べると、オブジェクト指向の究極的な目標はRapid ReleaseとDevOpsだ。オブジェクト指向で開発すると、以下のメリットがある。
1.素早く開発・リリースできる
2.開発と運用がスムーズに連携できる

どういうことか?
そこに行くには、まずオブジェクト指向で作られたプログラムの特徴を理解する必要がある。
僕の理解では、オブジェクト指向で作られたプログラムの特徴は以下の2点だ。

適切な範囲でクラス分けされていること
クラス分けをするということは、関係あるプロパティ、メソッド類が一つのクラスにまとまっている状態を指す。
オブジェクト指向の基本は以下の2つのステップだ。

  1. オブジェクトのプロパティに値を詰める
  2. そのオブジェクトに対して何かを実行する(.Runする)

例えば、生徒クラスがあれば、そこに[名前]と[年齢]のプロパティがあって、そこに[授業を受ける]、というメソッドがあったりする。クラス分けされていないコードはMain()に全てをダーッと書くようなコードになる。(これを僕は勘違いしていたのだけれど、Main()の中のコードの複雑な部分をMain()の外にメソッド分けしたって同じクラス内であれば実質同じことだ。)

テストが自動化されていること
修正が入っても、ボタン一つでテストを自動で行ってくれる。そしてそのためには、コードの中で適切にクラス分けがされている必要がある。どういう意味かというと、Main()に全てのコードを書いているコードはテストが自動化できないという致命的な欠点がある。Rapid Releaseにはテストの自動化が欠かせない。(ちなみに、テストの自動化がされていないコードをレガシーコードというらしい)。

オブジェクト指向で作られたプログラムとそうでないのを比較してみる

ここに本当に簡単なものだけど、電卓プログラムを作ってみた。(色々考慮が足りてない点があるけど、そこは目をつぶってください。。。)
オブジェクト指向サンプル
・四則演算のみできる
・ゼロ除算、もしくは不正な値が入力された場合エラーメッセージ、エラーラベルを表示する

ObjectOrientedとNonObjectOrientedの二つのプロジェクト、ObjectOrientedの方には自動化された単体テストプロジェクトが付属している。WindowsFormsプロジェクトなので、Main()ではなく、Button_ClickイベントがMain()に相当する。ここで気を付けてほしい点は、Main()に全てを書き連ねてしまったNonObjectOrientedでは単体テストが自動化できない、という点だ。ObjectOrientedの場合、Main()でやっていることは単純だ。

オブジェクト指向で作った場合

Modelに画面に入力された情報を詰めて、Validationして、問題がない場合は計算結果を表示している。実際の処理は隠ぺいされている。

ObjectOrientedのButtonClickイベント
Public Class ObjectOrientedForm
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        '画面に入力された情報をModelオブジェクトに詰める
        Dim model = Me.GetCurrentModel()
        'Modelオブジェクトに格納された情報に対してバリデーションチェックを行う
        Dim result As ValidationResult = model.ValidateModel()

        'バリデーションチェックの結果でエラーラベル、メッセージを表示する
        ErrorLabel.Visible = result.HasErrorRequiredInputData
        If result.IsRequredFieldError Then
            MsgBox("数字を入力してください")
            Exit Sub
        ElseIf result.IsDivisionByZero Then
            MsgBox("0で除算はできません")
            Exit Sub
        End If

        '計算結果を表示する
        Me.ResultTextbox.Text = model.Calculate
    End Sub
自動化された単体テストのコード
Imports System.Text
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports ObjectOriented

<TestClass()> Public Class UnitTest1
    <TestMethod()> Public Sub Devide左辺が10で右辺が5の場合結果は2になる()
        'Arrange
        Dim model As New Model

        model.Num1 = 10
        model.Num2 = 5
        model.Divide = True

        Dim expected As Double = 2
        Dim actual As Double = model.Calculate

        'Assert
        Assert.AreEqual(expected, actual)
    End Sub

オブジェクト指向で作った場合

Public Class NonObjectOrientedForm
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim num1 = 0
        Dim num2 = 0
        Dim result = 0

        If Double.TryParse(Number1Textbox.Text, num1) = False And
            Double.TryParse(Number2Textbox.Text, num2) = False Then
            ErrorLabel.Visible = True
            MsgBox("数字を入力してください")
            Exit Sub
        End If

        Dim radiobutton = (From f In Me.Controls
                           Where TypeOf f Is RadioButton
                           Where f.checked = True).FirstOrDefault

        Select Case radiobutton.Name
            Case "PlusRadiobutton"
                result = num1 + num2
            Case "MinusRadiobutton"
                result = num1 - num2
            Case "MultiplicationRadiobutton"
                result = num1 * num2
            Case "DivisionRadiobutton"
                If num2 = 0 Then
                    ErrorLabel.Visible = True
                    MsgBox("0で除算はできません")
                    Exit Sub
                End If
                result = num1 / num2
            Case Else
                result = 0
        End Select
        ErrorLabel.Visible = False
        ResultTextbox.Text = result
    End Sub

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.PlusRadiobutton.Checked = True
    End Sub
End Class

ビジネスロジックとメインロジックを分離する意味

テストプロジェクトを見てもらうとわかると思うけど、実際にテストしているのはmodel.Calculate()とmodel.ValidateModel()の部分だけだ。言ってみれば、実際のビジネスロジックの部分。イベントの中では処理を呼び出すだけにとどめて、ビジネスロジックは分離されている。ビジネスロジックの結果、どのように分岐し処理する、という部分はテストの対象範囲外となっている。
もちろん完全にやらないわけではなく、実際にはメインロジックの部分は手動テストや結合テストで実施することになる。ただ、考えてみれば、ビジネスロジックで変更・修正が入ることはままあっても、メインのロジックで修正が入ることはまれだと思う。自動化された単体テストはミリ秒レベル、手動テストは数分、場合によっては数時間から数日かかる。だから、変更頻度の高いビジネスロジック部分は自動化された単体テスト、変更があまり入らないメインロジック部分は手動テスト、という風にテストを分担している

そして、ここで重要なのは、modelをクラス分けして、[画面に入力された情報]、[それらの情報に対するチェックメソッド]という風にひとまとまりにして外だししていることで、初めて自動化された単体テストの対象にできるという点だ。インスタンス化できない部品は単体テストを自動化できない。(Formクラスをインスタンス化して、Main()メソッドをテストするのは難しすぎる。)わざわざステップ数を増やしたりしてまでクラス分けしているのは、畢竟自動化された単体テストのため、と言ってもよいと思う。