📝

継承を避けて合成で書く👀

に公開

はじめに

リファクタリング技術を勉強していた際に標題の手法がテーマに挙がっていたので、

今回はこれを記事にしてみたいと思います。

継承によるパターン

まずは以下のコードを見てください。

// 継承を用いるパターン

abstract interface class Bird {
  bool canFly();
  bool hasBeak();
}

class CommonBird implements Bird {
  
  bool canFly() => true;

  
  bool hasBeak() => true;
}

class Penguin extends CommonBird {
  
  bool canFly() => false;
}

interfaceを定義し、それを実装するようにCommonBirdクラスを定義後、

それを拡張するようにPenguinクラスを定義しています。

一見するとよくあるコードですね。

合成によるパターン

では次はこっちのコードを見てみましょう。

// 合成を用いるパターン

abstract interface class Bird {
  bool hasBeak();
  bool canFly();
}

class CommonBird implements Bird {
  CommonBird();

  
  bool hasBeak() => true;

  
  bool canFly() => true;
}

class Penguin implements Bird {
  Penguin();

  final CommonBird _commonBird = CommonBird();

  
  bool hasBeak() => _commonBird.hasBeak();

  
  bool canFly() => false;
}

interfaceであるBirdやそれを実装したCommonBirdクラスには変更はありませんが、

// 変更前
class Penguin extends CommonBird {
  
  bool canFly() => false;
}
// 変更後
class Penguin implements Bird {
  Penguin();

  final CommonBird _commonBird = CommonBird();

  
  bool hasBeak() => _commonBird.hasBeak();

  
  bool canFly() => false;
}

このようにPenguinクラスの定義が変わっていますね。

変更前はinterfaceを実装したCommonBirdクラスを拡張しているのに対し、

変更後は同じinterfaceであるBirdを実装するようになっています。

加えて変更後のコードではhasBeak()メソッドを実装するためにCommonBirdクラスのオブジェクトをプライベート化して作成し、

それを介してCommonBirdクラスのhasBeak()メソッドをcallするように実装していますね。

これは変更後のPenguinクラスはinterfaceを実装するクラスとして定義しているからであり、

変更前のPenguinクラスのようにCommonBirdクラスとは異なる振る舞いの部分だけを@overrideを使って上書きするようには定義できないからです。

合成による利点

一見すると変更前の方が修正が少なく直感的なコードであるように感じられるかと思われますが、

ここでこんなケースを考えてみましょう。

abstract interface class Bird {
  bool hasBeak();
  bool canFly();
  bool canSwim(); // 新規追加
}

interfaceに新しくcanSwim()という未実装のメソッドが追加されましたね。

ということはinterfaceを実装するクラスではこれを実装せねばなりません。

別の表現をすると...

実装していなければコンパイラエラーを出すことができる

と言うことになります。

これはどういうことかと言うと、まずは変更後の合成によるパターンを見てみると、

スクリーンショット 2025-11-08 18.15.52.png

このように実装していない箇所で未実装を示すエラーを吐くことができ、

こうすることで実装力の強制が生まれ、実装忘れによる想定外の振る舞いを防止することができます。

一方、継承によるパターンだと、

スクリーンショット 2025-11-08 18.23.36.png

このようにinterfaceであるBirdを実装するCommonBirdクラスでしかエラーを吐くことができません。

Penguinクラスはinterfaceを実装してはおらず、それを実装したクラスを拡張しているからです。

ここに問題点があります。

その理由は、

子クラスでの上書きは強制されず、コードの大規模化により忘れられる可能性が高くなる

と言うことが発生するからです。

つまり、

// 継承を用いるパターン

abstract interface class Bird {
  bool canFly();
  bool hasBeak();
  bool canSwim(); // 新規追加
}

class CommonBird implements Bird {
  
  bool canFly() => true;

  
  bool hasBeak() => true;

  
  bool canSwim() => false; // 新規追加
}

class Penguin extends CommonBird {
  
  bool canFly() => false;

  // 新規追加が必要ない場合、CommonBirdの実装をそのまま利用できる
  // と言うことはペンギンが泳げないことになってしまう。。。
}

このような歪なことになってしまいます。

子クラスは暗黙的に親クラスの振る舞いに引っ張られてしまう、つまり『ペンギンは泳げない』ということになってしうので、

その上書き忘れを苦慮して親クラスは常に子クラスの振る舞いを意識すると言ったことになってしまうと、

互いのクラスの結合度が高くなり変更容易性が低くなってしまいます。

大規模開発であったりコードのメンテナンスのことを考えると、人間は容易にコードの改修範囲が何処でどのように影響するのかを忘れてしまいます。

それ故に、

  • 実装の強制力
  • 抽象に依存することによる拡張性

こう言った強味を持つ合成を用いたパターンの方がコードをより良くしていく上では良いと思われます。

参考

https://book.mynavi.jp/ec/products/detail/id=147499

https://www.shuwasystem.co.jp/book/9784798073453.html

Discussion