Effective Java 第2版の項目16章、「継承よりコンポジションを選ぶ」を読んだので軽くまとめておきます。
問題提起
Setに挿入された回数を記憶する為に、HashSetを継承した以下のようなクラスを作成した。この実装方法は正しいか?
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet() {
}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@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;
}
}
上手く動作しない!(問題点)
以下のように実行してみる。
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("AAA", "BBB", "CCC");
System.out.println(s.getAddCount());
このとき、s.getAddCount()は6を返す。要素を3つしか追加していないのに何故か?
その理由はHashSetが内部的にaddを呼び出しているから。add()とaddCount()で二重にaddCountが加算されている。
このInstrumentedHashSetを正しく動作させる為には、HashSetのaddAllメソッドがallメソッドを使用しているという事実を実装者が知っている必要がある。サブクラスとスーパークラスの実装が同じプログラマの管理下であれば安全だが、そうでない場合は脆いものになる。
また、スーパークラスの実装がリリース毎に変更され、サブクラスのコードが変更されていなくても機能しなくなるかもしれないという問題もある。
コンポジションを使う
このようなケースでは、コンポジション(composition)と転送(forwarding)を使ってクラスを実装すると良い。
尚、コンポジションとは、あるクラスに別のクラスのオブジェクトを取り込んで扱うことを言う。尚、委譲(delegate)は自分自身のオブジェクトを渡して別のクラスに処理を任せることを言う。
// 転送クラス
public class ForwardingSet<E> implements Set<E> {
// コンポジション
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
public void clear() {
s.clear();
}
public add(E e) {
return s.add(e);
}
public addAll(Collection<? extends E> c) {
return s.addAll(c);
}
...
}
このクラスを継承してラッパークラスを実装する。
// ラッパークラス
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;
}
}
このような書き方をすると、以下のような利点がある。
- このクラスが持つSetの実装を意識せずに、新たに自分が拡張した処理(addCount)を実装できる
- スーパークラスの実装がリリース毎に変更されたとしても、このクラスには影響が無い(インターフェースが変更されない限り)
- Setインターフェースを実装するすべてのクラスに対してこのクラスが使用出来る(Setの実装クラス毎に継承する必要が無い)
以下のように実行する。
Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp));
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));
継承の使いどころ
継承はいつ使えば良いのか?という疑問が残る。継承元クラスと継承したクラスの関係が、本当のサブタイプ関係があるときだけ、継承は使うべき。
ただ拡張したいだけであれば上記のコンポジションの方法を使って実装する。
さいごに
「InstrumentedSetクラスが計測を追加することでセットを装飾するので、デコレータ(Decorator)パターンとしても知られています。
(Effective Java 第2版 P84)
と書いてあるけど、DecoratorはForwardingSetではない?Decoratorパターンについてどこを参照すればいいか分からないけど、
Decorator パターンの方針は、既存のオブジェクトを新しい Decorator オブジェクトでラップすることである。
(WikipediaのDecorator パターンの項より)
というのが定義なら、Setの参照を持っているForwardingSetがDecoratorな気がするんだけども。