拡張性/柔軟性を獲得する章
3.4. 継承の悪い例
『目的』
継承がコード再利用に対して万能ではないことを知りましょう。
『Before』
継承前提になっていないクラスを継承してはいけません。
HashSetに対して、 どれだけの要素が挿入されたかカウントできるように継承を使って拡張してみます。
public class HashSetEx<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 getCount() { return addCount; } }
要素挿入処理の際に、カウンタの増分処理を組み込んでみました。 しかし、この実装は正しく動作しません。 実はHashSet#addAllメソッドは内部でaddメソッドを呼び出しているのです。 なので、二重カウントされてしまいます。
でも、addAllメソッドがaddメソッドを呼び出すとはHashSetの説明には書いてありません。 ドキュメントとして明文化されていない場合は継承することは危険です。
では、二重カウントされないように addAll のオーバーライドをやめたらどうなるでしょうか。
public class HashSetEx<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 getCount() { return addCount; } }
正しく動作はします。
ただし、addAllメソッドがaddメソッドを呼び出しているということに依存しています。 将来、addAllメソッドがadd メソッドを呼び出さなくなる可能性もあるわけで、 未来永劫正しく動作する保証はありません。変更にもろいわけです。
では、addAllメソッドを自前で実装してaddメソッドを呼び出すようにしたらどうなるでしょうか。 そうすれば、HashSetのaddAllメソッドがadd メソッドを使わなくなったとしても動きますね。 でも、元々あったメソッドを再実装してしまうのに等しくなります。 しかも、元のメソッドがサブクラスからアクセスできないprivateフィールドにアクセスしていたら、 もはや実装の道も閉ざされてしまいますね。
というわけで、継承では「もろさ」が残ってしまいます。
あと、オーバーライドに関連する問題もあります。
サブクラス側で新しいメソッドを追加していたのに、スーパークラス側で同一シグニチャの メソッドが定義されてしまったとします。 戻り値が異なれば、サブクラスはコンパイルできなくなってしまいます。 戻り値が同じだとしたら、サブクラスで「偶然」オーバーライドしている状態になります。
『After』
こういった問題を解決するには「コンポジション」と「転送」と呼ばれる方法を使うことで解決できます。 デザインパターンのDecoratorで扱います。
『まとめ』
継承前提ではないクラスを継承して拡張しようとすると弊害が起こってしまうことがあります。
気をつけましょう。
また、このようなことを防ぐために継承前提ではないクラスを作成する場合はクラスの修飾子としてfinalを付けることをおすすめします。
-
IS 559680 / ISO27001
認証範囲 本社、芝大門オフィス情報セキュリティ基本方針 -
個人情報保護方針
個人情報の取扱いについて
保有個人データ又は
第三者提供記録に関する
事項の周知について
特定個人情報の取扱いについて -
FS 671851 / ISO9001
認証範囲 本社、芝大門オフィス品質方針