メソッドを呼び出すことができると定義するよりも、メソッドをオーバーライドできると定義する方が強いコミットメントはありますか?


36

から:http : //www.artima.com/lejava/articles/designprinciples4.html

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

私は彼が何を意味するのか理解していません。誰か説明していただけますか?

回答:


63

コミットメントは、将来の選択肢を減らすものです。メソッドを公開すると、ユーザーがメソッドを呼び出すことになるため、互換性を損なうことなくこのメソッドを削除することはできません。あなたがそれを保持した場合private、彼らはそれを(直接)呼び出すことができず、いつか問題なくそれを離れてリファクタリングすることができました。したがって、メソッドを公開することは、公開しないことよりも強力です。オーバーライド可能なメソッドの公開は、さらに強力なコミットメントです。ユーザーはそれを呼び出すことができ、そして、彼らは方法は、あなたがそれがないと思う何をしない新しいクラスを作成することができます!

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

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


11
これはおそらく、私が今まで読んだ継承に対する最良の議論です。私が遭遇したすべての理由のうち、これらの2つの議論に出会ったことは一度もありません(オーバーライドによる機能の結合と破壊)が、どちらも継承に対する非常に強力な議論です。
デビッドアルノ

5
@DavidArno継承に対する議論だとは思わない。「すべてをデフォルトでオーバーライド可能にする」ことに対する議論だと思います。継承はそれ自体では危険ではなく、考えずに使用することは危険です。
svick

15
これは良い点のように聞こえますが、「ユーザーが自分のバグのあるコードを追加する可能性がある」という議論がどのように行われているのか、実際にはわかりません。継承を有効にすると、ユーザーはバグを防止および修正できる手段である更新可能性を失うことなく、不足している機能を追加できます。APIの上部にあるユーザーコードが破損している場合、それはAPIの欠陥ではありません。
Sebb

4
この引数を簡単に変えることができます。最初のコーダーはクリーンアップ引数を作成しますが、間違いを犯し、すべてをクリーンアップしません。2番目のコーダーはクリーンアップメソッドをオーバーライドし、適切に機能します。コーダー#3はクラスを使用し、コーダー#1が混乱してもリソースリークは発生しません。
ピーターB

6
@Doval確かに。だからこそ、継承がほぼすべての入門OOPの本とクラスでレッスン1番であるというのは悲惨なことです。
ケビンクルムウィーデ

30

通常の関数を公開する場合、一方的なコントラクトを提供します:
呼び出された場合、関数は何をしますか?

コールバックを公開する場合、一方的なコントラクトも提供します。
いつ、どのように呼び出されるのですか?

オーバーライド可能な関数を公開する場合は、両方同時に実行されるため、両側のコントラクトを提供します:
いつ呼び出され、呼び出されたら何をしなければならないのでしょうか?

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

このような両面契約にrenegingの例は、から動きであるshowhideまでsetVisible(boolean)のjava.awt.Component


+1。他の答えが受け入れられた理由がわかりません。興味深い点がいくつかありますが、引用された箇所が意味するものではないという点で、この質問に対する正しい答えではありません。
ルアー

これは正しい答えですが、例がわかりません。表示と非表示をsetVisible(boolean)に置き換えると、継承も使用しないコードが壊れるようです。何か不足していますか?
eigensheep

3
@eigensheep:showhideまだ存在して、彼らはただです@Deprecated。そのため、変更によって、単にそれらを呼び出すだけのコードが壊れることはありません。ただし、それらをオーバーライドした場合、新しい 'setVisible'に移行するクライアントからオーバーライドは呼び出されません。(私はそれがそれらを上書きすることがいかに共通知らないので、私は、スイングを使用したことがありません。しかし、それは長い時間前に起こったので、デュプリケータは、それはそれは痛いほど彼/彼女ビットということで覚えていることを、私はその理由を想像してみてください。)
ルアック

12

Kilian Fothの答えは素晴らしいです。これが問題である理由の標準的な例*を追加したいだけです。整数の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()を実装することは不可能です。

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

よく記述された契約の好例はComparatorです。.equals()上記の理由で、それが言うことを無視してください。ここだコンパレータは、物事を行うことができる方法の一例.equals()することはできませんが

ノート

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

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

ボーナス

コンパレータは、equals()を正しく実装する問題を回避するため、興味深いものです。さらに良いことに、このタイプの継承の問題を修正するためのパターン、つまり戦略設計パターンに従います。HaskellとScalaの人々が興奮するTypeclassは、Strategyパターンでもあります。継承は悪いことでも間違っていることでもありません。注意が必要です。詳細については、Philip Wadlerの論文「アドホックな多型をアドホックにしない方法」を参照してください。


1
equalsただし、SortedMapとSortedSetは、MapとSetが定義する方法の定義を実際には変更しません。等価性は、順序付けを完全に無視します。たとえば、同じ要素を持つがソート順序が異なる2つのSortedSetsが依然として同等であるという効果があります。
user2357112は

1
@ user2357112そのとおりです。その例を削除しました。SortedMap.equals()がMapと互換性があることは別の問題であり、これについて文句を言います。通常、SortedMapはO(log2 n)であり、HashMap(Mapの標準実装)はO(1)です。したがって、順序付けを本当に重視する場合にのみ、SortedMapを使用します。そのため、SortedMap実装のequals()テストの重要なコンポーネントになるほど順序が重要だと思います。Mapとequals()実装を共有しないでください(JavaのAbstractMapを介して共有します)。
グレンピーターソン

3
「継承は悪いことでも間違っていることでもありません。注意が必要です。」私はあなたの言っていることを理解していますが、トリッキーなことは通常、エラー、バグ、問題につながります。同じこと(またはほとんどすべて同じこと)をより信頼性の高い方法で達成できる場合、よりトリッキーな方法悪いです。
jpmc26

7
これはひどい例です、グレン。継承を使用すべきではない方法で使用しただけで、クラスが意図したとおりに機能しないのも不思議ではありません。間違った抽象化(2Dポイント)を提供することでLiskovの置換原則を破りましたが、間違った例で継承が悪いからといって、一般的に悪いというわけではありません。この答えは理にかなっているように見えますが、最も基本的な継承ルールに違反していることに気付いていない人を混乱させるだけです。
アンディ

3
LiskovのSubsubituton PrincipleのELI5は次のように述べています:クラスBがクラスの子であり、クラスAのオブジェクトをインスタンス化する必要がある場合B、クラスBオブジェクトをその親にキャストし、キャストされた変数のAPIを使用することができます。子供。3番目のプロパティを提供して、ルールを破りました。変数をにzキャストした後、基本クラスにそのようなプロパティが存在することがわからない場合、どのように座標にアクセスする予定ですか?子クラスをそのベースにキャストすることでパブリックAPIを破ると、抽象化が間違ってしまいます。Point3DPoint2D
アンディ

4

継承の脆弱性のカプセル化

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

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

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);
    }
}

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

継承により拡張性が失われる

クラスをサブクラス化できるようにすると、クラスにメソッドを追加しないことになります。そうでなければ、何も壊さずにこれを行うことができます。

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


3

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

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

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

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

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

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

したがって、a明確に明確に定義され、十分に文書化され、「1つのことを行い、それをうまく行う」ことが、呼び出し専用に設計されたメソッドである場合よりも重要です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.