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

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

6章 継承によって振る舞いを獲得する

クラスによる継承を理解する

インスタンス変数に「スタイル」「型」「カテゴリー」といった属性がある場合、「クラスによる型わけと継承」でそれを実現できないか考慮する。

抽象的なスーパークラスを作るさいは、そこに属するクラスが3種類以上になってから考慮し始めると、正しい抽象を得られる確率が高くなりやすい。もちろん、2種類で最善だと思うならば、トライしてみるのも選択の一つだ。

抽象的なクラスを作るときのポイントは、一度すべてのコードをサブクラスに移し、そこから抽象的な部分の抽出のみに力を入れること。スーパークラスに信頼できない具体的な行動が残ってしまえば、その継承者すべてに負担がかかる。

スーパークラスが、「サブクラス全てについて実装してほしい処理」を持つ場合は、エラー文などを実装することで、サブクラスの実装のさいに起きうるエラーについて、情報を明示的に与えるようにするほうがいい。

def default_tire_size
  raise NotImplementedError
    "This #{self.class} can not respond to:"
  end
end

# 親クラスでdefault_tire_sizeを呼び出す処理を書いたが、そのメソッドを子どもに持ってほしい場合のエラー文の例
# あくまで例なので、こんな実装をして子どもに負担をかけるぐらいならデフォルト値を入れてあげたほうが良いだろう。

テンプレートメソッドパターンを使う

スーパークラスで定められているデフォルト値を設定するメソッドは、サブクラスで容易にオーバーライドできる。

フックメソッドを自分で実装する

サブクラスにsuperといった「親クラスのアルゴリズム理解」を求めるコードは結合性をまし、危険となりうる。

以下のようなpost_initializeメソッドを実装すれば、下位クラスでsuperといった上位クラス呼び出しを準備する必要がなくなり、疎結合性が高まる。

def Bicycle

  def initialize(args={})
    @size = args[:size]
    @chain = args[:chain] || default_chain
    @tire_size = args[:tire_size] || default_true_size

    post_initialize 
  end

  def post_initialize(args)
    nil 
  end 

end

class RoadBike < Bicycle

  def post_initialize(args)
    @tap_color = args[:tape_color]
  end
  #...
end

インスタンス変数のセットの場合は上の例でよい。

親クラスのメソッドをオーバーライドしながら、サブクラス固有の動きを実現する場合にsuperを使いたくない場合は、以下のようなテクニックがある。(子クラスは、sparesメソッドをもらいながら、フックであるlocal_sparesに処理を記述することで、superを使わずに自らの動きを実現する)

class Bicycle

#...

  def spares
    { tire_size: tire_size,
      chain:         chain}.merge(local_spares)
  end

  def lolal_spares
    {}
  end

#...

end

たとえば、下位クラスでlocal_sparesメソッドを定義し、その中に任意のハッシュ(例:{handle_size: 20})を入れておくと、下位クラスのインスタンスがsparesメソッドに答えるとき、その値がマージされた状態で呼び出せるといった動きになる。

このようなフックメソッドを丁寧に実装すれば、サブクラスはテンプレートメソッドにさえ(post_initializeやlocal_spares)注意を払えば問題なくなる。

まとめ

継承は共通のアルゴリズムを束ね、隔離する手段であり、サブクラスで具体的な事象に特化することが出来る仕組みである。

抽象的なスーパークラスを作るか判断するための一番の方法は「3つ以上の具体的なクラスが同じふるまいをそれぞれもっているとき」だ。

感想

フックメソッドの処理とか知らなかったら絶対自分で思いつかないテクニックだなー…。

ただ、注意しないと親クラスがテンプレートメソッド持ってるとか忘れそうな気もする。 initialize_post とかはまだしも、local_sparesメソッドとかチーム内で「localってprefixがついてるメソッド(local_bar等)は親クラスのbarメソッドにフックするよ」みたいな認識取れてないと事故りません?

そのへんは共通認識なのかな?