[Effective Java 第3版]項目18 - 継承よりもコンポジションを選ぶ

継承はコードを再利用するには便利な機能ですが、常に既存のメソッドを継承し、拡張することがベストな方法とは限りません。
サブクラスとスーパークラスが同じ開発者の管理下にあり、同じパッケージ内にある場合は比較的安全といえます。
しかしパッケージをまたがり具象クラスを継承することは危険です。

継承の不適切な使用例

HashSetが生成させてからいくつ要素が追加されたかを確認するため、getAddCountを実装します。
要素の挿入回数 addCountをフィールドに保持し、HashSetクラスの要素を追加できるaddとaddAllをオバーライドし要素が追加されるとaddCountをカウントアップします。

public class InstrumentedHashSet<E> extends HashSet<E> {

   // 要素の挿入回数
   private int addCount = 0;

   @Override
   public boolean add(E e) {
       addCount++;
       return super.add(e);
   }

   @Override
   public boolean addAll(Collection<? extends E> c) {
       addCount += c.size();
       return super.addAll(c);
   }

   public int getAddCount() {
       return addCount;
   }
}

この実装では、addAllが呼ばれると期待する結果通り動作しません。
要素を3つ追加すると、getAddCountで6が返ってきます。

public class App {

   public static void main(String[] args) {
       InstrumentedHashSet<String> set = new InstrumentedHashSet<String>();
       set.addAll(Arrays.asList("AAA", "BBB", "CCC"));

       System.out.println(set.size()); //→3
       System.out.println(set.getAddCount()); //→6
   }
}

addAllメソッドは内部でaddメソッドを使って実装されています。
InstrumentedHashSetのaddAllを呼び出すと、addCountに要素の数を足して、スーパークラスのaddAllを呼び出します。
HashSetのaddAllでは、個々の要素に対して、InstrumentedHashSetでOverrideされているaddを呼び出すため、2重にカウントアップされてしまいます。

実装の修正を検討

新たにメソッドを追加することで、この問題は解消されるますが、安全とは言えません。
スーパークラスが後にメソッドを追加して、同じシグニチャでことなる戻り値を定義すると、サブクラスのメソッドはコンパイルできなくなることもあります。

コンポジション

上述する問題を解消することができるのがコンポジション(composition)という考え方です。
既存クラスを拡張するのではなく、新たなクラスに既存のクラスのインスタンスをprivateフィールドで持たせ、既存クラスが新たなクラスの構成要素となります。
新たなクラスのメソッドは既存クラスのインスタンスに対して、メソッドを読み出しそのまま結果を返します。これを転送(fowarding)と呼びます。
コンポジションを利用すれば、新たなクラスは既存のクラスの実装の詳細に依存することがなくなります。

//ラッパークラス-継承の代わりにコンポジションを使用
public class InstrumentedSet<E> extends ForwardingSet<E> {
   private int addCount = 0;

   public InstrumentedSet(Set<E> s) {
       super(s);
   }

   @Override
   public boolean add(E e) {
       addCount++;
       return super.add(e);
   }

   @Override
   public boolean addAll(Collection<? extends E> c) {
       addCount += c.size();
       return super.addAll(c);
   }

   public int getAddCount() {
       return addCount;
   }
}
//再利用可能な転送クラス
public class ForwardingSet<E> implements Set<E> {
   //既存クラスをpravateフィールドで持つ
   private final Set<E> s;

   public ForwardingSet(Set<E> s) {
       this.s = s;
   }

   @Override
   public boolean add(E e) {
       return s.add(e);
   }

   @Override
   public boolean addAll(Collection<? extends E> c) {
       return s.addAll(c);
   }

//他SetクラスのメソッドのOverrideは省略・・・
public class App {

   public static void main(String[] args) {
       Set<String> hashSet = new HashSet<String>();
       InstrumentedSet<String> set = new InstrumentedSet<String>(hashSet);
       set.addAll(Arrays.asList("A", "B", "C"));

       System.out.println(set.size()); //→3
       System.out.println(set.getAddCount()); //→3
   }
}

継承とコンポジションの使い分け

継承

サブクラスがスーパークラスのサブタイプである場合に拡張するべきです。
サブクラスとスパークラスとの間に「is-a」の関係が存在しているか考え、満たす場合拡張するようにします。
「B is a A」は「BはAである」という意味となります。
「犬は動物である」、「車は乗り物である」のような関係を持っている場合、継承を使うとよいでしょう。

コンポジション

継承の「is-a」に対して「has-a」の関係がある場合コンポジションを使うとよいと考えられています。
「B has a A」は「BはAを含んでいる」という意味となります。
「タイヤは車に含まれている」、「メモリはPCに含まれている」のような関係を持っている場合、コンポジションを使うとよいでしょう。

また以下のような場合にもコンポジションが推奨されているようでした。

・明確に「is-a」の関係が成立すると言えない
・is-aの関係は成立するが、パッケージをまたがる継承となってしまう、またはスーパークラスの実装を明確に理解できない

参考

https://thekingsmuseum.info/entry/2015/09/16/003849
https://techlib.circlearound.co.jp/entries/extends-vs-composition/
https://4geek.net/difference-between-inheritance-and-composition/


この記事が気に入ったらサポートをしてみませんか?