拡張性/柔軟性を獲得する章

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を付けることをおすすめします。

< 前のページへ

Pagetop