具象メソッドのオーバーライドはコードの匂いですか?


31

具体的なメソッドをオーバーライドするのはコードの匂いだというのは本当ですか?なぜなら、具体的なメソッドをオーバーライドする必要があると思うからです:

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

次のように書き換えることができます

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

BがAでa()を再利用したい場合、次のように書き換えることができます。

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

メソッドをオーバーライドするために継承を必要としないのは本当ですか?



4
(以下のコメントで)ダウン投票されたのは、TelastynとDoc Brownがオーバーライドに同意しますが、「コードのにおい」の意味に同意しないため、見出しの質問に反対のyes / noの答えを与えます。そのため、コード設計に関する質問は、スラングと密接に結びついています。そして、質問は具体的にはある技術と別の技術に関するものだからです。
スティーブジェソップ

5
質問にはJavaタグは付いていませんが、コードはJavaのようです。C#(またはC ++)では、virtualキーワードを使用してオーバーライドをサポートする必要があります。そのため、問題は言語の詳細によってより曖昧になります。
エリックエイド

1
ほぼすべてのプログラミング言語の機能は、十分な年月を経て、必然的に「コード臭」として分類されるようになります。
ブランドン

2
このようなfinal状況では、修飾子が正確に存在するように感じます。メソッドの動作がオーバーライドされるように設計されていない場合、としてマークしますfinal。それ以外の場合、開発者がそれをオーバーライドすることを選択できることを期待してください。
マーティンタスケビキウス

回答:


34

いいえ、コードの匂いではありません。

  • クラスが最終クラスでない場合、サブクラス化することができます。
  • メソッドが最終的なものではない場合、オーバーライドできます。

サブクラス化が適切かどうか、およびどのメソッドがオーバーライドされる可能性があるかを慎重に検討することは、各クラスの責任内にあります

クラスは、それ自体またはメソッドをfinalとして定義するか、サブクラス化の方法と場所に制限(可視性修飾子、利用可能なコンストラクター)を設定できます。

メソッドをオーバーライドする通常のケースは、サブクラスでカスタマイズまたは最適化できる基本クラスのデフォルト実装です(特にJava 8では、インターフェイスにデフォルトメソッドが出現します)。

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

動作をオーバーライドするための代替手段Strategy Patternです。この場合、動作はインターフェースとして抽象化され、実装はクラスで設定できます。

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}

現実の世界では、具体的なメソッドをオーバーライドすることは一部の人にとってはコードの匂いかもしれないことを理解しています。しかし、私はこの答えが好きです。なぜなら、それは私に基づいたより多くの仕様のようだからです。
-Darkwater23

あなたの最後の例では、A実装してはいけませんDescriptionProviderか?
発見

@Spotted:A 実装DescriptionProviderできるため、デフォルトの説明プロバイダーになります。しかし、ここでの考え方は懸念の分離です:A他の実装がそれらを提供する一方で、説明(他のクラスも必要かもしれません)が必要です。
ピーターウォル

2
わかりました、戦略パターンにもっと似ています
発見

@Spotted:そのとおり、この例ではデコレーターパターンではなく戦略パターンを示しています(デコレーターはコンポーネントに動作を追加します)。答えを訂正します、ありがとう。
ピーターウォル

25

具体的なメソッドをオーバーライドするのはコードの匂いだというのは本当ですか?

はい、一般的に、具体的なメソッドをオーバーライドすることはコードのにおいです。

基本メソッドには、開発者が通常尊重する振る舞いが関連付けられているため、実装を変更すると、変更するとバグが発生します。さらに悪いことに、それらが動作を変更する場合、以前の正しい実装は問題を悪化させる可能性があります。また、一般的に、非自明な具体的な方法については、リスコフの代替原則を尊重することはかなり困難です。

現在、これを行うことができ、良い場合があります。半自明なメソッドは、ある程度安全にオーバーライドできます。オーバーライドされること意図した、「正常な」デフォルトの動作としてのみ存在する基本メソッドには、いくつかの優れた用途があります。

したがって、これは私には臭いのように見えます-時々良い、通常は悪い、確認するために見てください。


29
あなたの説明は素晴らしいですが、私は正反対の結論に来ます:「いいえ、具体的なメソッドをオーバーライドすることは一般にコードの匂いではありません」-オーバーライドするつもりのないメソッドをオーバーライドするときのコードの匂いです。;-)
Doc Brown

2
@DocBrownまさに!また、説明(および特に結論)が最初の声明に反することもわかります。
テルサウロス

1
@Spotted:最も単純な反例は、値を次の有効な値に設定Numberするincrement()メソッドを持つクラスです。1つではなく2つを追加するEvenNumber場所にサブクラス化する場合increment()(およびセッターが均一性を強制する場合)、OPの最初の提案では、クラス内のすべてのメソッドの重複した実装が必要であり、2つ目の提案では、メンバーのメソッドを呼び出すラッパーメソッドを記述する必要があります 継承の目的の一部は、異なる必要があるメソッドのみを提供することにより、子クラスが親とどのように異なるかを明確にすることです。
Blrfl

5
@DocBrown:何かを意味するものではありませんコードのにおいをあることはある必ずしもそれがあるだけで、悪い可能性が高いです
ミコワク

3
@docbrown-私にとっては、これは「継承よりも有利な構成」と並んでいます。具体的なメソッドをオーバーライドしている場合、継承を持っているためにすでに小さな臭い土地にいます。あなたがすべきではない何かをオーバーライドしている可能性は何ですか?私の経験に基づいて、それは可能性が高いと思います。YMMV。
Telastyn

7

具体的なメソッドをオーバーライドする必要がある場合[...]

そのように書き換えることができる場合は、両方のクラスを制御できることを意味します。しかし、クラスAをこの方法で派生するように設計したかどうかはわかります。そのようにした場合、それはコードの匂いではありません。

一方、基本クラスを制御できない場合は、そのように書き換えることはできません。したがって、クラスはこの方法で派生するように設計されていますが、その場合は先に進むか、コードの匂いではないか、そうではありません。この場合、2つのオプションがあります。別のソリューションを探すか、とにかく先に進む作業はデザインの純度よりも重要だからです。


クラスAが派生するように設計されていた場合、意図を明確に表現する抽象メソッドを使用する方が理にかなっていますか?メソッドをオーバーライドした人は、メソッドがこのように設計されたことをどのようにして知ることができますか?
発見

@Spottedでは、デフォルトの実装が提供され、機能を拡張するため、または効率を上げるために、各専門分野がそれらの一部をオーバーライドするだけでよい場合が多くあります。極端な例は訪問者です。ユーザーは、ドキュメントからメソッドがどのように設計されたかを知っています。いずれにせよ、オーバーライドに何が期待されるかを説明するために存在しなければなりません。
Jan Hudec

私はあなたの視点を理解していますが、メソッドをオーバーライドできることを知るために開発者が持っている唯一の方法はドキュメントを読むことであると危険だと思います:1)オーバーライドする必要がある場合、それが保証されないするつもり。2)オーバーライドしてはならない場合、誰かがその便宜のためにそれを行います3)全員がドキュメントを読むわけではありません。また、メソッド全体をオーバーライドする必要はなく、一部のみをオーバーライドする必要に直面したことはありません(テンプレートパターンの使用を終了しました)。
発見

@ Spotted、1)オーバーライドする必要がある場合は、abstract; しかし、そうである必要がない多くの場合があります。2)オーバーライドしてはならない場合は、final(非仮想)にする必要があります。しかし、メソッドがオーバーライドされる場合がまだ多くあります。3)下流の開発者がドキュメントを読まなかった場合、彼らは自分自身を撃ちますが、どのような対策を講じてもそれを行います。
Jan Hudec

わかりましたので、私たちの視点の相違点は1単語だけにあります。すべてのメソッド抽象的または最終的である必要があると言いますが、すべてのメソッド抽象的または最終的でなければなりません。:)メソッドのオーバーライドが必要な(そして安全な)これらのケースは何ですか?
発見

3

まず、この質問は特定のタイピングシステムでのみ適用可能であることを指摘しておきます。例えば、内構造タイピングダックタイピングシステム-単にクラス有する同名&と方法を署名はなる平均そのクラスであった互換性のある型(ダックタイピングの場合には、少なくともその特定のメソッドのために)。

これは(明らかに)Java、C ++などの静的に型付けされた言語の領域とは大きく異なります。また、JavaとC ++、およびC#などの両方で使用される強い型付けの性質も異なります。


Javaのバックグラウンドから来ているのではないかと推測しています。コントラクトデザイナーがメソッドをオーバーライドしないように意図している場合、メソッドは明示的にfinalとしてマークする必要があります。ただし、C#などの言語では、反対がデフォルトです。メソッドは(装飾や特別なキーワードなしで)「封印」(C#のバージョン)であり、オーバーライド許可されているかのように明示的に宣言する必要がありますfinalvirtual

APIやライブラリは、具体的な方法でこれらの言語で設計されている場合、それ必要があります(少なくともで、オーバーライドいくつかのそれが上書きされるためにケース(S))メイク感覚。したがって、unsealed / virtual / not finalconcreteメソッドをオーバーライドするのはコードの匂いではありません。     (これはもちろん、APIの設計者が慣例に従っており、virtual(C#)としてマークfinalするか、設計しているものに適切なメソッドを(Java)としてマークしていることを前提としています。)


こちらもご覧ください


ダックタイピングでは、同じメソッドシグネチャを持つ2つのクラスが型互換性を保つのに十分ですか、または暗黙的な基本クラスの代わりにliskovに置き換えられるように、メソッドが同じセマンティクスを持つ必要がありますか?
ウェインコンラッド

2

はい、それはスーパーアンチパターンの呼び出しにつながる可能性があるためです。また、メソッドの最初の目的を悪化させ、副作用(=>バグ)を導入し、テストを失敗させる可能性があります。ユニットテストが赤(失敗)のクラスを使用しますか?しません。

最初のコードの匂いは、メソッドがそうでabstractないときfinalです。構築されたエンティティの動作を(オーバーライドすることにより)変更できるため(この動作は失われたり変更されたりする可能性があります)。abstractし、final意図は明確な次のとおりです。

  • 要約:動作を提供する必要があります
  • 最終:動作を変更することはできません

既存のクラスに動作を追加する最も安全な方法は、最後の例で示したものです:デコレータパターンを使用します。

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}

9
この白黒のアプローチは、それほど便利ではありません。Object.toString()Javaで考えてみてください...これがの場合abstract、多くのデフォルト設定は機能せず、誰かがtoString()メソッドをオーバーライドしていないとコードはコンパイルされません!もしそうならfinal、事態はさらに悪化するでしょう-Javaの方法を除いて、オブジェクトを文字列に変換する能力はありません。以下のよう@Peterヴァルザーについての答え会談、デフォルトの実装(などはObject.toString()ある重要。特にJava 8のデフォルトメソッドでは。
テルソサウルス

@TersosaurosもしそうtoString()だったのかabstractfinalそれが痛みだったら、あなたは正しいです。私の個人的な意見では、このメソッドは壊れていて、まったく持たない方がいい思います(equalsやhashCodeなど)。Javaデフォルトの最初の目的は、レトロ互換性を備えたAPIの進化を可能にすることです。
発見

「レトロコンパチビリティ」という言葉には、非常に多くの音節があります。
クレイグ

私は非常にこのサイト上の具体的な相続の一般的な受け入れに失望してい@Spotted、私はより良い予想:/あなたの答えは、より多くのupvotesを持つべきである
TheCatWhisperer

1
@TheCatWhisperer私は同意しますが、一方で、具体的な継承よりも装飾を使用することの微妙な利点を見つけるのに時間がかかりました(実際、最初にテストを書き始めたときに明らかになりました) 。最善の方法は、真実(冗談)が見つかるまで他の人を教育することです。:)
発見
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.