ソフトウェア設計

コンポジション(Composition)と継承(Inheritance):「継承よりコンポジションを優先せよ」の本当の意味

オブジェクト指向を学んでいると、必ず次の言葉に出会います。「継承よりコンポジションを優先せよ(Favor composition over inheritance)。」

読了 5 分
コンポジション(Composition)と継承(Inheritance):「継承よりコンポジションを優先せよ」の本当の意味のカバー画像

コンポジションと継承、「継承を使うな」の本当の意味

オブジェクト指向を学んでいると、必ず次の言葉に出会います。「継承よりコンポジションを優先せよ(Favor composition over inheritance)。」

結論から言うと、これは「継承を絶対に使うな」という意味ではありません。「コードの再利用を目的に継承を乱用するな」という意味です。継承は今でも有効な手段ですが、再利用が必要な場面の多くでは、コンポジションのほうが安全です。

この記事では、実際のコードで両者の違いがどう現れるのか、またいつどちらを選ぶべきかを、私の経験を交えて説明します。

継承とコンポジション、何が違うのか

まずはとてもシンプルに分けて考えてみましょう。

継承は「〜は〜である(is-a)」という関係です。たとえば、犬は動物です。子クラスは親クラスの機能をそのまま受け継ぎます。

コンポジションは「〜は〜を持つ(has-a)」という関係です。車はエンジンを持つ、という具合です。必要な機能を持つオブジェクトを内部に保持し、そのオブジェクトに処理を委譲します。

コードで見ると、よりイメージしやすくなります。以下は継承によって機能を受け継ぐ方法です。

// 継承: Stackは NSMutableArrayのすべてのメソッドを継承
class Stack: NSMutableArray {
    func push(_ o: Any) { add(o) }
    func pop() -> Any {
        let o = lastObject!
        removeLastObject()
        return o
    }
}

この方法では、Stackにとって不要なinsert(_:at:)やremoveObject(at:)のようなメソッドまで外部に公開されてしまいます。

以下は同じものをコンポジションに置き換えた形です。

// コンポジション:必要な機能だけを選んで委譲
class Stack {
    private var list: [Any] = []
    func push(_ o: Any) { list.append(o) }
    func pop() -> Any { list.removeLast() }
}

配列を内部に保持し、必要な操作だけを外部に公開しています。Stackが本当に必要とする機能だけが残ります。

継承はすべてを取り込み、コンポジションは必要なものだけを取り込む
継承はすべてを取り込み、コンポジションは必要なものだけを取り込む

「継承を使うな」の本当の意味

この言葉が生まれた背景には、継承の代表的な弱点が2つあります。

1つ目は、カプセル化が崩れることです。子クラスは親クラスの内部実装に依存します。親のコードが変わると、問題なく動いていた子クラスが突然壊れることもあります。

2つ目は、結合度が高くなりすぎることです。親と子がコンパイル時に強く結び付くため、後から関係を変更するのが難しくなります。

継承はコードを再利用するための道具ではなく、型を定義するための道具です。

この一文が核心です。単にコードを再利用したいという理由で継承を使った瞬間、関係が複雑になり始めます。

有名な例が正方形と長方形です。数学的には正方形は長方形なので、継承すればよさそうに見えます。しかし、長方形の「幅と高さを別々に変更する」機能を受け継いだ瞬間、正方形はもはや正方形ではなくなります。

is-a関係に見えても、振る舞いまで完全に置き換えられないなら、継承は落とし穴になります。


では、継承はいつ使ってよいのでしょうか?

継承が常に悪いわけではありません。次の条件をすべて満たすなら、継承のほうがかえってきれいに書けます。

  1. 本当のis-a関係か:子クラスは常に親クラスの一種ですか?
  2. リスコフの置換原則を守っているか:親クラスの代わりに子クラスを置いても問題ありませんか?
  3. 親クラスは継承を前提に設計されているか:ドキュメントがあり、拡張のために開かれていますか?

3つとも当てはまるなら、継承を使っても問題ありません。UIViewControllerのようなフレームワークベースのクラスを拡張する場合が代表例です。

逆に、1つでも曖昧なら、まずコンポジションを考えてみてください。

表で簡単に比較してみました。

状況 推奨
純粋なis-a関係で、完全に置換可能 継承
単にコードを再利用したいとき コンポジション
実行時に振る舞いを変更したいとき コンポジション
複数の機能を組み合わせる必要があるとき コンポジション

このように、実務で遭遇する状況の多くはコンポジションに傾きます。「favor composition」という助言が生まれたのには理由があります。

表にすると、いつ何を使うべきかが一目でわかります
表にすると、いつ何を使うべきかが一目でわかります

実務での私の判断基準

新しいクラスを作るとき、私は習慣的にまず「これは親クラスの一種なのか、それとも親の機能を借りたいだけなのか」と考えます。

借りたいだけなら、ほぼ必ずコンポジションを選びます。

デザインパターンを見ても方向性は明らかです。StrategyパターンやDecoratorパターンなどは、すべてコンポジションを基盤にしています。

特にStrategyパターンは、振る舞いをオブジェクトに分離し、実行時に差し替えます。継承では再現しにくい柔軟性です。

ブロックを差し替えるように変更できることが、コンポジションの魅力です
ブロックを差し替えるように変更できることが、コンポジションの魅力です

もちろん、コードが少し長くなることは受け入れる必要があります。委譲メソッドを自分で書かなければならないからです。それでも、後から構造を変更するのははるかに簡単です。


「継承を使うな」という言葉は、継承を禁止するのではなく、再利用目的での乱用に注意しようという助言です。is-aが明確なら継承、単に機能を借りたいだけならコンポジション。この1つの基準を意識するだけで、コードはずっと堅牢になります。今日もよい設計を!

あわせて読みたい