4-18:継承よりもコンポジションを選ぶ
要点
継承(extends)は「is-a」関係/多態性のために使い、再利用のためのコード共有はできるだけコンポジション(委譲)で行う。つまり「継承よりもコンポジションを選ぶ」が安全で柔軟な設計の基本。
なぜコンポジションを優先するのか
- カプセル化を壊さない: 継承はスーパークラスの実装詳細(protected メンバなど)に依存しがちで、内部実装変更でサブクラスが壊れる。
- 柔軟性が高い: ランタイムに部品(delegate)を差し替えられる(Strategy / Decorator 等)。
- API 決定を先延ばしできる: 公開 API を増やさずに内部実装を入れ替えられる。
- テストしやすい: 内部の委譲オブジェクトをモックに差し替えられる。
- 継承の誤用を防ぐ: 継承はサブクラスに多くの責任を課す。単にコードを使いたいだけなら委譲の方が安全。
悪い例
public class Stack<E> extends Vector<E> {
public E push(E item) { addElement(item); return item; }
public synchronized E pop() {
if (isEmpty()) throw new EmptyStackException();
return remove(size() - 1);
}
}
問題点:
- Vector の公開メソッド(例えば remove(int) など)によりクライアントが Stack の状態を直接壊せる(LIFO を保証できない)。
- Stack は「is-a Vector」に見えるが、意味的には「スタック(LIFO)を使っている(has-a)」のほうが自然。
良い例
public class Stack<E> {
private final List<E> elements = new ArrayList<>();
public void push(E e) { elements.add(e); }
public E pop() {
if (elements.isEmpty()) throw new EmptyStackException();
return elements.remove(elements.size() - 1);
}
public boolean isEmpty() { return elements.isEmpty(); }
}
利点:
- Stack の公開 API を自分でコントロールできる(不要なメソッドを露出しない)。
- 内部実装(ArrayList→LinkedList)は将来好きに変えられる。
- テスト時に elements を差し替えたい場合はコンストラクタ注入を使えば容易。
よく使われるコンポジションパターン
- デコレータ(Decorator): 既存オブジェクトに機能を付加(BufferedInputStream が InputStream をラップ)。
- ストラテジー(Strategy): 振る舞いをオブジェクトとして差し替え(ソートアルゴリズム等)。
- フォワーディング(Forwarding): 内部オブジェクトにメソッドを委譲して必要な API のみ公開。
- ファサード/アダプタ: 複数オブジェクトをまとめて単純なインタフェースを提供。
// forwarding の例(簡略)
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public boolean add(E e) { return s.add(e); }
public boolean remove(Object o) { return s.remove(o); }
// ... 他メソッドを必要に応じて委譲
}
まとめ
継承は強力だが脆い。まずはコンポジション(委譲)で再利用・拡張を行い、本当にサブタイプ化すべきときだけ継承を使う。 これによりカプセル化と柔軟性を保ち、将来の変更コストを下げられる。