オブジェクト指向設計実践ガイドの7章、8章を読んだ

あとから自分で読み直せるように、ブログにまとめつつ読んでいます。

7章 モジュールでロールの振る舞いを共有する

ロールを理解する

モジュールによって振る舞いを共有する。 

オブジェクトに自身を語らせる

そのオブジェクトへの質問を、第3者に委譲しない。

継承可能なコードを書く

スーパークラスを継承したにもかかわらず、スーパークラスの振る舞いを拒否するようなサブクラスは作るべきではない。

そのようなサブクラスが増えれば、継承に関して信頼を持てなくなり、保守することが難しくなる。

まとめ

モジュールによる処理の共通化は便利だけど、メソッドの探索経路が複雑化しすぎないように注意しよう。

感想

メソッド探索は以前メタプログラミングRuby読んだときに理解してたから不足感あったなー逆に。 ただ、extendの挙動について、改めて整理できたから良かったかも。

extendはsingleton_classに対してのincludeみたいな挙動だな。(そのオブジェクトに対しての特異クラスの親側へのメソッド継承)

探索で一番優先されるのはそのオブジェクト自身の特異クラスってのは変わらなさそう。

8章 コンポジションでオブジェクトを組み合わせる

Arrayを継承せずにArrayのように振る舞う

Arrayのサブクラスではないが、Arrayのような扱いをしたいクラスについて、以下のコードによって実現することができる。

require 'forwardable'

class Parts
  extend Forwardable   #def_delegatorメソッドを貰う
  def_delegators :@parts, :size, :each #第一引数のインスタンス変数に対して、sizeメソッドとeachメソッドを譲渡。
  include Enumerable

  def initialize(parts)
    @parts = parts
  end

  def spares
    select { |part| part.needs_spare}
  end
end

ここの理解が結構難しかった!!

でもたぶんわかったのでまとめます。

もし委譲の処理を書かなかった場合、Parts.newに対してsizeといったメソッドは打ってもエラーが返る。 その理由は、Partsクラスのインスタンスのメソッド探索経路にsizeが存在しないからだ。

ただし、上記のように、「def_delegators」を定義することによって、第二引数以降のメソッドの処理(今回だとsizeとeach)がParts.newに打たれた場合、その処理を第一引数に委譲することができる。(今回の場合だと、Parts.newがもつインスタンス変数@parts)

ということで、Parts.new.sizeは、Parts.newが持つインスタンス変数@partsに対するsizeを打つといった挙動に変化する。

コードで書くなら「Parts.new.size」は、「Parts.new.parts.size」の処理になる。 (今はParts.newにpartsは打てないが、ゲッターメソッドを定義していたとしたらこんな挙動だろう)

たぶんこれで合ってるはず!!

つまり、parts = Parts.new([foo, zoo, boo, bar])したとき、parts.sizeで、partsの@partsにsizeして4が返るってわけだな。 テクい。

ただ、関係ないかも知れないけど疑問が一つあって、Rails使ってるとモデル名を複数形にするのはご法度感あるけど大丈夫なのかということ。 Partsモデルのコントローラとかどうなるんでしょう…partsesコントローラーですかね…。

コンポジションで解決を試みる

継承によるメッセージの獲得は「拡張は簡単で、修正は難しい」

is-a関係(XXは○○である)といった関係なら、継承が望ましいかも知れない。

has-a関係(XXは○○を持っている)といった関係なら、コンポジションが望ましいかも知れない。

まとめ

コンポジション、クラス継承、モジュールによる振る舞いの共有は、それぞれ差異がある。 テクニックなので慣れていこう。

感想

設計の話だけど、コンポジション化していくテクニックが高等すぎて「おお…」って感じの内容だった。 include Enumerableは実際にそれを取り入れてる現場のコードを見てみたいなあ。 噂には聞いたことがあったから使いこなしてみたい。