読者です 読者をやめる 読者になる 読者になる

オブジェクト指向とは結局メンタルモデルのモデリング手法である

きしださんのエントリが話題です。

オブジェクト指向は禁止するべき - きしだのはてな

「禁止するべき」とはまた随分と煽りタイトルですねと思いつつも、内容自体はとても納得のいくものでした。

ただ「オブジェクト指向」というのはいろいろな観点で語られることが多く、多少モヤモヤとはしているので僕の考えを書いてみようと思います。

なお、きしださんご自身は以下の補足エントリで立場は明確にされています。本エントリはこれを否定するものではありません。あくまで違った立場からの意見です。*1

オブジェクト指向について - きしだのはてな

参考までに、ぼくの基本的な定義は、ランボーの「データ構造と振る舞いが一体となったオブジェクトの集まりとしてソフトウェアを組織化すること」という定義に従っています。そのようなオブジェクトが単体ではなく組織化されるということが重要です。オブジェクト指向を勉強するとはそのような組織化のしかたを勉強することだと考えています。

7/26 12:52 追記

きしださんがもっと端的に言いたいことをツイートしていたので追記です。

わかりやすいw

元エントリの自分なりの理解

元エントリが具体的に何を批判しているのか少しわかりづらかったので、結構何度も読んだのですが、まとめると

  • データと振る舞いが一体である必要がない局面は多い
  • 重要なのはシステムの複雑度を下げることであり、そのために向き合うべきはプログラミング言語の機能

という2点が主な主張なのかなと思いました。

これはどちらも納得のいく主張で、振る舞いを持たないデータコンテナ的なクラスもあれば、データはなくstaticな(引数によってのみ戻り値が決定するステートレスな)メソッドのみを持つサービスクラス的なものもあることが普通であり、それを無理にオブジェクト指向っぽく(データと振る舞いが一体となるように)設計すると逆に複雑になるということなのかなと思います。

それに対する解決策のひとつがデザインパターンの類なのかなとは思っていて、例えばデータコンテナ的なクラスはDTO(Data Transfer Object)と呼ばれたりバリューオブジェクト*2パターンで実現されたりしますし、逆に振る舞いのみを持つクラスはまさにサービスパターンとして実現されます。("サービス"という言葉は PoEAA とエリックエヴァンスの DDD でまったく違う意味として使われるので、個人的にはあまり使いたくない言葉ですが…ステートレスであるという点は共通しているはずです。)

個人的にはこういったデザインパターンの適用である程度までは複雑性をコントロールできると思っているのですが、元エントリでは"プログラミングに不慣れな人"も対象としているようで、そういう人にとっては適切なパターンを適用できないことが多いということなのかと思いました。

つまり

  1. データだけ、振る舞いだけを必要とする局面あるよ
  2. その場合はそういうクラスを作るよ(この時点ですでに"オブジェクト指向"的ではないよ)
  3. プログラミングに慣れた人はそれでいいけど、不慣れな人はもっとファンタジックなほうがわかりやすいから"オブジェクト指向"にこだわっちゃうよ

ということなのかと。

メンタルモデルへの近似

なんかここまで書いて納得しちゃったのでもういいんですが、これだと元エントリを解説しただけなのでもうちょっと書きます (笑) ここから先は完全に僕の意見になります。(元エントリ関係ない)

オブジェクト指向であること自体は重要でなく、システムの複雑性を下げることが重要であるという点については完全に同意で、であれば「オブジェクト指向的、つまりデータと振る舞いが一体となっているほうが複雑度が下がるのはどういったケースなのか」について論じたいと思います。

僕の結論は「データと振る舞いを一体としたほうが利用者のメンタルモデルにあっているかどうか」です。

例えば、最近よくオブジェクト指向と対比されている関数型プログラミングとの比較で考えてみます。

以下のコードは整数をラップした A と B というクラスに対してその値を2倍するというシンプルな処理を、インスタンスメソッドと関数で実行したものになります。

オブジェクト指向と関数型の両方のパラダイムを持つ Scala で実装してみました。)

object Main {
    class A (val v:Int) {
      def doubleMethod() = v * 2
    }

    class B (val v:Int)
    def doubleFunction (b:B) = b.v * 2

    def main(args: Array[String]): Unit = {
        // インスタンスメソッド(オブジェクト指向っぽい)
        val a = new A(10)
        val doubleA = a.doubleMethod()
        println(doubleA)

        // 関数(関数っぽい)
        val b = new B(10)
        val doubleB = doubleFunction(b)
        println(doubleB)
    }
}

これらは完全に等価であり、少なくとも機能面からは両者に優劣はありません。*3

どちらの実装がより好ましいかを判断する基準は利用者のメンタルモデルとどの程度近似してるかであり、A や B といった名詞が概念として中心にありそれに対して振る舞いが定義されるのであればオブジェクト指向的なほうがよいですし、そうではなく振る舞いのほうが中心(上記のサービスパターンのような感じ)であれば関数的な実装のほうがよいでしょう。

オブジェクト指向の設計原則がメンタルモデルに反することもある

オブジェクト指向設計にはいくつかの設計原則や先述したようなデザインパターンの類がありますが、それがメンタルモデルと反することもしばしばあります。

例えば、GRASP の責務の割り当てパターンのひとつに「情報エキスパート」というのがあります。

GRASP - Wikipedia

情報エキスパートパターンは、オブジェクトに責務を割り当てる際の一般的な原則である。情報エキスパートパターンは、情報のエキスパート、すなわち必要な情報を全て持っているクラスに責務を割り当てるべきとする。

これは多くの場合において設計を支援する原則ですし、データと振る舞いと一体であるべきとするオブジェクト指向の考え方からすると至極まっとうなように思います。

ただ、実世界のモノとその間の協調関係を忠実にモデリングしようとすると必ずしも情報エキスパートが振る舞いを持っているとは限らず、情報を持ったオブジェクトをメソッドの引数として渡すほうが自然なことは多々あります。 このような場合、自分であれば(原則に反しますが)メンタルモデルを重視して設計することが多いように思います。

また、GRASP で挙げられる他のパターン(生成者、コントローラ等)はメンタルモデル中に登場しないことが多く、純粋人工物(DDDでいうところのサービス)にいたっては完全に振る舞いそのものであり、無理やりオブジェクトにしたにすぎません。

こういうケースでは元エントリの主張の通り無理にオブジェクト指向にこだわる必要はないと思いますし、上述した Scala のように別のプログラミングパラダイムを持つプログラミング言語を利用できるのであれば、そちらを利用して設計したほうがよいように思っています。

概念が安定している場合にはオブジェクト指向は強い、しかし世の中は複雑である

概念が安定していて、興味の対象であるモノの役割や意図が明確か、明確でなくとも思考の対象として十分に機能するものであれば、オブジェクト指向は便利だし、オブジェクト指向設計は表現力の高い設計手法だと思います。

ただ、世の中は複雑で、安定しない概念、移ろいやすい事象をソフトウェアで表現しないといけないケースもありますし、先に述べたように実装の都合でファクトリーやコントローラー、リポジトリなどの人工物を定義しないといけないケースもあります。

近年の各言語の発展のしかたを見ると、そういった場合にオブジェクト指向以外の選択肢をとれるということが非常に重要になってきているのかなとは思います。

(僕は個人的に DDD の考え方が好きなので考え方がそちらに寄っているというのは認めます。)

*1:そもそも元エントリで紹介されている書籍を読んだことがないので、同じ立場では語れませんorz

*2:バリューオブジェクトは振る舞いを持つことはあるが、状態は変わらないのでデータコンテナの一種と考えることができる。

*3:オブジェクト指向におけるインスタンスメソッドは、暗黙的に自分自身が引数として渡される(Pythonっぽい!)と考えれば関数とそれほど大差ありません。