web-dev-qa-db-ja.com

メソッドを呼び出すことができることを定義するよりも、メソッドをオーバーライドできることを定義すると、どのようにして強いコミットメントをオーバーライドできますか?

差出人: http://www.artima.com/lejava/articles/designprinciples4.html

エーリッヒ・ガンマ:10年経った今でもそうだと思います。継承は、動作を変更するためのクールな方法です。しかし、サブクラスは、オーバーライドするメソッドが呼び出されるコンテキストについて簡単に想定できるため、脆弱であることはわかっています。私がプラグインしたサブクラスコードが呼び出される暗黙のコンテキストがあるため、基本クラスとサブクラスの間には密結合があります。コンポジションはより良い特性を持っています。小さなものを大きなものに接続するだけで結合が減少し、大きなオブジェクトは小さなオブジェクトを呼び出すだけです。 APIの観点から、メソッドをオーバーライドできることを定義することは、メソッドを呼び出すことができることを定義することよりも強いコミットメントです。

彼の意味がわかりません。誰か説明していただけますか?

36
q126y

コミットメントは、将来のオプションを減らすものです。メソッドを公開すると、ユーザーがそれを呼び出すことになるため、互換性を損なうことなくこのメソッドを削除することはできません。 privateを保持していると、(直接)呼び出すことができず、いつの日か問題なくリファクタリングできます。したがって、メソッドを公開することは、公開しないことよりも強い責任です。 オーバーライド可能メソッドの公開は、さらに強力な取り組みです。ユーザーはそれを呼び出すことができ、およびメソッドが意図したとおりに動作しない新しいクラスを作成できます!

たとえば、クリーンアップメソッドを公開すると、ユーザーがこのメソッドを最後に呼び出すことを忘れない限り、リソースが適切に割り当て解除されるようにすることができます。しかし、メソッドがオーバーライド可能な場合、誰かがサブクラスでそれをオーバーライドして、notsuperを呼び出す可能性があります。その結果、3番目のユーザーがそのクラスを使用してリソースリークを引き起こす可能性があります最後にcleanup()を忠実に呼び出したとしても!つまり、コードのセマンティクスを保証できなくなります。これは非常に悪いことです。

基本的に、ユーザーがオーバーライド可能なメソッドで実行されているコードに依存することはできなくなります。これは、一部の仲介者がオーバーライドしてしまう可能性があるためです。つまり、クリーンアップルーチンを完全にprivateメソッドに実装する必要があり、ユーザーの助けは必要ありません。したがって、APIユーザーによるオーバーライドが明示的に意図されていない限り、通常はfinal要素のみを公開することをお勧めします。

63
Kilian Foth

通常の関数を公開する場合、片側契約を行います。
関数が呼び出された場合はどうなりますか?

コールバックを公開する場合、片側契約も行います。
いつ、どのように呼び出されますか?

また、オーバーライド可能な関数を公開する場合、両方が同時に実行されるため、両側のコントラクトを指定します。
いつ呼び出され、どのような場合に呼び出されますか?

ユーザーがAPIを悪用していない場合でも契約の一部を破ることで(検出が非常に高価になる可能性があります)、簡単に確認できます後者ははるかに多くのドキュメントが必要であり、すべてドキュメント化はコミットメントであり、これはあなたのさらなる選択を制限します。

このような両面コントラクトを無視する例は、_ Java.awt.Component でのshowおよびhideからsetVisible(boolean)への移動です。

30
Deduplicator

キリアン・フォスの答えは素晴らしい。これが問題である理由の正規の例*を追加したいと思います。整数のPointクラスを想像してください:

_class Point2D {
    public int x;
    public int y;

    // constructor
    public Point2D(int theX, int theY) { x = theX; y = theY; }

    public int hashCode() { return x + y; }

    public boolean equals(Object o) {
        if (this == o) { return true; }
        if ( !(o instanceof Point2D) ) { return false; }

        Point2D that = (Point2D) o;

        return (x == that.x) &&
               (y == that.y);
    }
}
_

それを3Dポイントになるようにサブクラス化しましょう。

_class Point3D extends Point2D {
    public int z;

    // constructor
    public Point3D(int theX, int theY, int theZ) {
        super(x, y); z = theZ;
    }

    public int hashCode() { return super.hashCode() + z; }

    public boolean equals(Object o) {
        if (this == o) { return true; }
        if ( !(o instanceof Point3D) ) { return false; }

        Point3D that = (Point3D) o;

        return super.equals(that) &&
               (z == that.z);
    }
}
_

超シンプル!私たちのポイントを使ってみましょう:

_Point2D p2a = new Point2D(3, 5);
Point2D p2b = new Point2D(3, 5);
Point2D p2c = new Point2D(3, 7);

p2a.equals(p2b); // true
p2b.equals(p2a); // true
p2a.equals(p2c); // false

Point3D p3a = new Point3D(3, 5, 7);
Point3D p3b = new Point3D(3, 5, 7);
Point3D p3c = new Point3D(3, 7, 11);

p3a.equals(p3b); // true
p3b.equals(p3a); // true
p3a.equals(p3c); // false
_

なぜ私がそのような簡単な例を投稿しているのか疑問に思われるでしょう。ここにキャッチがあります:

_p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!
_

2Dポイントを同等の3Dポイントと比較するとtrueになりますが、比較を逆にするとfalseになります(p2aが_instanceof Point3D_で失敗するため)。

結論

  1. 通常、サブクラスでメソッドを実装することは、スーパークラスが期待する方法との互換性がなくなるような方法で実装することができます。

  2. 親クラスと互換性のある方法で大きく異なるサブクラスにequals()を実装することは、一般的に不可能です。

人々がサブクラス化できるようにするクラスを書くとき、各メソッドがどのように振る舞うべきかについてcontractを書くことは本当に良い考えです。オーバーライドされたメソッドの実装に対して人々が実行できる一連の単体テストは、契約に違反していないことを証明するためにさらに優れています。それはあまりにも多くの仕事なので、ほとんど誰もそれをしません。しかし、気にかけているなら、それを行うべきです。

よく綴られた契約の良い例は Comparator です。上記の理由により、.equals()についての説明は無視してください。これが Comparatorが.equals()ができないこと の例です。

ノート

  1. Josh Blochの「Effective Java」の項目8はこの例のソースですが、Blochは、3番目の軸の代わりに色を追加し、intの代わりにdoubleを使用するColorPointを使用しています。 BlochのJavaの例は、基本的に Odersky/Spoon/Venners によって複製されており、その例をオンラインで公開しています。

  2. 親クラスにサブクラスについて知らせれば、この問題を修正できるので、何人かの人々がこの例に反対しました。サブクラスの数が十分に少なく、親がそれらすべてを知っている場合は、これが当てはまります。しかし、最初の質問は、誰かがサブクラスを作成するためのAPIを作成することでした。その場合、通常、親実装を更新してサブクラスと互換性を持たせることはできません。

ボーナス

Equals()を正しく実装するという問題を回避できるため、コンパレータも興味深いものです。さらに良いことに、このタイプの継承の問題を修正するためのパターン、つまり戦略デザインパターンに従います。 HaskellとScala人々が興奮するTypeclasssもStrategyパターンです。継承は悪いことでも間違っていることでもありません。それはただのトリッキーです。詳細については、Philip Wadlerの論文 アドホックなポリモーフィズムをアドホックから少なくする方法

12
GlenPeterson

継承はカプセル化を弱める

継承が許可されたインターフェースを公開すると、インターフェースのサイズが大幅に増加します。オーバーライド可能な各メソッドは置き換えることができるため、コンストラクターに提供されるコールバックと考える必要があります。クラスによって提供される実装は、コールバックのデフォルト値にすぎません。したがって、メソッドに対する期待が何であるかを示す何らかの契約を提供する必要があります。これはめったに発生せず、オブジェクト指向コードがもろいと呼ばれる主な理由です。

以下は、Javaコレクションフレームワーク、Peter Norvig( http://norvig.com/Java-iaq.html )の厚意による)の実際の(簡略化した)例です。

Public Class HashTable{
    ...
    Public Object put(K key, V value){
        try{
            //add object to table;
        }catch(TableFullException e){
            increaseTableSize();
            put(key,value);
        }
    }
}

これをサブクラス化するとどうなりますか?

/** A version of Hashtable that lets you do
 * table.put("dog", "canine");, and then have
 * table.get("dogs") return "canine". **/

public class HashtableWithPlurals extends Hashtable {

    /** Make the table map both key and key + "s" to value. **/
    public Object put(Object key, Object value) {
        super.put(key + "s", value);
        return super.put(key, value);
    }
}

バグがあります。「dog」を追加すると、ハッシュテーブルが「dogss」のエントリを取得することがあります。原因は、Hashtableクラスの設計者が予期していなかったputの実装を誰かが提供したことです。

継承は拡張性を壊します

クラスのサブクラス化を許可する場合は、クラスにメソッドを追加しないことを約束します。それ以外の場合は、何も壊すことなく実行できます。

インターフェイスに新しいメソッドを追加する場合、クラスから継承したすべてのユーザーがそれらのメソッドを実装する必要があります。

4
eigensheep

メソッドを呼び出すことを意図している場合は、それが正しく機能することを確認するだけで済みます。それでおしまい。できました。

メソッドがオーバーライドされるように設計されている場合は、メソッドのスコープについても慎重に検討する必要があります。スコープが大きすぎる場合、子クラスは親メソッドからコピーして貼り付けたコードを含める必要があります。小さすぎる場合、多くのメソッドをオーバーライドして、必要な新しい機能を実現する必要があります。これにより、複雑さと不要な行数が追加されます。

したがって、親メソッドの作成者は、クラスとそのメソッドが将来どのようにオーバーライドされるかを想定する必要があります。

しかし、著者は引用されたテキストで別の問題について話している:

ただし、サブクラスは、オーバーライドするメソッドが呼び出されるコンテキストについて簡単に想定できるため、脆弱であることはわかっています。

通常はメソッドaから呼び出されるメソッドbを検討しますが、まれにメソッドcから自明でない場合があります。オーバーライドメソッドの作成者がcメソッドとaに対するその期待を見落とす場合、どのように問題が発生するかは明らかです。

したがって、aを明確かつ明確に定義し、十分に文書化して、「1つの処理を実行し、適切に実行する」ことが重要です。これは、呼び出し専用に設計されたメソッドの場合よりも重要です。

3
Rainy