継承よりもコンポジション(委譲)

参考書籍一覧(Amazon アソシエイトリンク)

「継承は使うな」と強い言葉と合わせて継承よりもコンポジションという言葉が使われるが、一律で継承を禁止しコンポジションにすべきというわけではない。gotoのように節度のない使い方をすれば破綻してしまうために強い言葉が使われがちというだけである(gotoも適切な使い所はあったが全面禁止になってしまった)。

使い所を間違うとたやすくプログラムが壊れるという例を示す(Effective Java 第2版 p.80のコードを参考にした)。

class Base
  def initialize
    @list = []
  end

  def add(item)
    @list << item
  end

  def add_all(items)
    items.each do |item|
      @list << item
    end
  end
end

# 追加した個数をカウントする派生クラス。今のところ正しく動く。
class Sub < Base
  def initialize
    super
    @count = 0
  end

  def add(item)
    @count += 1
    super
  end

  def add_all(items)
    @count += items.length
    super
  end
end

Subクラスでは要素を追加するたびに追加した個数をカウントアップするようになっている。もし、Base#add_allメソッドが以下のようにリファクタリングされたとしたら、どうなるだろうか。

def add_all(items)
  items.each do |item|
    add(item) # 内容が同じなのでaddメソッドを呼ぶようにした
  end
end

add_allメソッドからaddメソッドを呼ぶようになった結果、Sub.new.add_all([1,2,3])とした時にadd_allからSub#addメソッドが呼ばれてしまい、@countは6になってしまう。

これは、継承する場合は基底クラスの内情を知っていなければならないことを意味する。そして、基底クラス側はカプセル化されているはずの内情を公開して、さらにそれが変更されないことを保証しなければならないことになる。

ここから言えることは、継承されることを想定されていないクラスは継承してはならないということだ。継承するなら、継承されることを想定して内情について何を保証するのか適切にドキュメント化されているクラスだけからのみにしなければならない。

ここまでの参考文献: Effective Java 第2版 p.80-85 項目16 継承よりコンポジションを選ぶ, p.86-90 項目17 継承のために設計および文書化する、でなければ継承を禁止する

ここからは私が感じていることである。

スペシャルケース(SpecialCase)パターンないしNullObjectパターンは脆いクラス構造だと言える。通常、NullObjectパターンを適用される基底クラスは継承されることを想定していない。インターフェースや握り潰すことが想定されるメソッドをまとめた抽象クラスを用意したりはしない。そのため、基底クラスの変更によって、握り潰すべき挙動が漏れたり、間違った握りつぶし方をしてしまうかもしれない。

これらのパターンはJavaのような公称型の言語で基底クラスの変更ができないケースでは仕方がないのかもしれないが、構造的部分型かダックタイピングな言語ならインターフェースを実装したProxyパターン(nullの時はNullObjectの挙動をして、そうでなければ本来のクラスに委譲する)にした方が良いだろう。

継承の話ではなくポリモーフィズムの話であるが、Stateパターンのような状態遷移という1つの条件分岐が複数のクラスにバラバラにされてしまうようなことはするべきではない。状態遷移はすべてのStateに関する縦の関心事であり、横の関心事である各Stateの切り方に巻き込んでしまうのは間違っている。Stateパターンではなく各State用のStrategyクラスを用意して、状態遷移は別のクラスが担うようにした方が良い。