web-dev-qa-db-ja.com

インターフェイスへのコーディングと抽象継承

インターフェイスへの継承とコーディングは、適切なアーキテクチャ設計に関して疑問に思っていたものですが、自分のプロジェクトでインターフェイスへのコーディングに対して抽象的な継承を使用する場合、実際には問題に遭遇していません。

SOLID OOP設計の原則(および一般的なOOPアドバイス))によると、インターフェースにコーディングし、使用しないでください継承(抽象またはその他)(そしてそうです、私は絶対的な文脈で話をしていることに気づきます。それ以外の場合、引数がほとんどないためです)これは、次のようなクラスがあるような状況では完全に理にかなっています。

class A {}

class B : A {}

class C : B {}

class D : C {}

class E : D {}

...

このコンテキストでは、クラス間に強力な結合があり、どのレベルでも簡単に壊れることがあります。継承が重要な問題であることに100%同意します。しかし、私はしばしば以下が継承の許容できる使用になるかどうか疑問に思います

public abstract class Vehicle
{
    public enum Gear
    {
        Drive,
        Reverse,
        Park,
        Low,
        High,
        Neutral
    }

    public string Color { get; set; }
    // Other common properties here …

    public virtual void Drive()
    {
        if (KeyInIgnition() && HasFuel() && SeatBeltsOn() && GearMode == Gear.Drive)
        {
            // Some logic to move car forward
        }
        // other logic ...
    }

    public virtual void Reverse()
    {
        // Logic here ...
    }
    // ...
}

public class Sedan : Vehicle
{
    // Set base class properties here ...

    public int UniqueSedanMethod() // Let’s say this method does not fit the standard to put something in an interface (i.e. it is completely unique to a sedan)
    {
        // Logic here ...
        return 3;
    }

    // Gets base class logic because driving a sedan does not differ (in the basics) from the driving of an SUV
}

public class SUV : Vehicle
{
    public int CalcAdditionalSeatsAdded()
    {
        // Logic here ...
        return 5;
    }
}

この文脈では、すべての車両が共有する共通の動作をグループ化するための論理的な基本クラスを持つことは、私には理にかなっているようです。ドライブの種類は車の種類に関係なく同じです。ただし、必要が生じた場合は、メソッドを完全にオーバーライドできます(オーバーライドで必要な場合は、デフォルトの実装を使用します)。これは明確、クリーン、シンプル、そしてもちろんDRYコードなので、これはベストプラクティスのようです。ただし、これにより、セダン、SUVなどがVehicleベースクラスに密結合されます。

反対の見方をすると、インターフェイスを使用した場合、それらがすべて同じであるにもかかわらず、実装のために各クラスに数行をコピーして貼り付ける必要があります。これは、DRY)の重大な違反であり、その後、一般的な動作が変更されると(つまり、より多くの作業とエラーが発生しやすくなる場合)、すべてのクラスのメソッドを修正または更新する必要があります。

別の反対の見方をすると、Drive、Reverseなどのメソッドを、何らかの形式のVehicleを受け入れるサービスのようなクラスに配置すると、2つの大きな問題がまだ発生します。

  1. 一般的なパラメーターを取得するためだけに、セダン/ SUVを共通のインターフェイスにコーディングする必要があります。そうでない場合は、なんらかの制約が必要なジェネリックを使用する必要があります(ジェネリックを最初に冗長にするインターフェイスまたはクラス全体のタイプ)クラスにメソッドを渡すことができるので、これは本当に理想的ではありません。
  2. セダン/ SUVは、これ以外の場合は無関係な依存関係サービスクラスに依存します(パラメーターの逆も同様です)。これは依然として密結合であるか、むしろ依存しています。

最初の問題の別の観点から、さまざまなメソッドを使用して、各車両タイプ(つまり、SedanService、SUVServiceなど)の外部サービスのようなクラスを作成できますが、これにより、サービスはそれぞれのタイプに依存します。依存関係の逆転の原則が実際に達成しようとしているものです(少なくとも私はそう思います)。 DRYコードがないという問題は、実際の車両タイプクラスの詳細をサービスクラスに入れることと交換するだけなので、再び発生します。これは実際に問題を解決するのではなく、かなりの「デッキチェアをシャッフルして、結果が良くなることを願う」という事業戦略。

この例と、インターフェース/依存関係の逆転の原理の使用に関する前述の視点では、インターフェースへのコーディングの利点、またはデフォルトの実装を保持するだけでよい共通の抽象基本クラスの継承を使用することに対する利点が理解できません。まれな状況で、ベースや他の派生物に影響を与えることなくオーバーライドされます。主な欠点は、ベースとデリバティブの緊密なカップリングであり、デリバティブが継承された場合に脆弱な継承チェーンを導入するpotentialが導入されます(つまり、デリバティブが継承される理由です)。例、私はしませんでした)。

以上を踏まえて、私の質問は次のとおりです。

「インターフェイスへのコードで継承を使用しない」という概念や依存関係の逆転の原則を誤解していますか? (私はこれら2つを誤用していますか?)その場合、正しい解釈と例/解決策は何ですか?

そうでない場合、依存関係の逆転/疎結合を実現するために、私が考えていない(または認識していない)/理解している別の方法はありますか?

「インターフェイスへのコード」、依存関係の逆転、およびより良いソリューションを認識せず、継承を正しく利用していないと誤解していない場合、継承は{ここに否定および/または意味を表す形容詞を挿入}と見なされますか?私が考慮していない欠点はありますか?

DRYコードと将来の保守性は、移植性とスケーラビリティのための疎結合のために犠牲にされるべきですか?)極端に慎重に考慮された計算なしに行うには本当に急なトレードオフのようです。

追伸.

インターフェイスは一般に、何かと一連の確立された特性と動作の間の契約を確立するためのものであることを理解しています。これは、クラスが「a(n) {インターフェース名/ここに名詞を挿入} "関係。対照的に、継承は、基本クラスと派生物に関して、クラスが" a(n) {基本クラス名/ここに名詞を挿入} "であることです。クラスの関係これは私が用語を解釈している方法なので、これは私の混乱の原因になるかもしれませんが、私は本当にそうは思いません。

6
G.T.D.

「インターフェイスへのコードを使用し、継承を使用しない」という概念を誤解していますか?

はい、あなたは2つの異なる原則を混同しているように見えるので、あなたはそれを誤解しています。

一方で、「インターフェースへのコード」の原則があり、C#やJavaなどの言語にinterfaceキーワードがあることは非常に残念です。原則の意味です。インターフェイスへのコーディングの原則は、クラスのユーザーは、どのパブリックメンバーが存在するかを知るだけでよく、メソッドがどのように実装されているかを知る必要がないことを意味します。interfaceキーワードは、ここで助けますが、その原則は言語設計にキーワードや概念さえも持たない言語にも等しく適用されます。

一方、「継承よりも構成を優先する」という原則があります。
この原則は、オブジェクト指向デザインが普及し、4レベル以上の継承が使用され、レベル間に適切で強力な「is-a」関係がない場合の継承の過度の使用に対する反応です。
個人的には、継承はデザイナーのツールボックスにあり、「継承を使用しない」と言う人は振り子を反対方向に振りすぎています。ただし、唯一のツールがハンマーの場合、すべてが釘のように見え始めることにも注意する必要があります。


質問の車両の例に関して、VehicleのユーザーがSedanSUVのどちらを処理しているかを知る必要も、メソッドの方法を知る必要もない場合Vehicle作業の場合、インターフェースへのコーディングの原則に従っています。
また、ここで使用される単一レベルの継承は私にとっては問題ありません。ただし、別のレベルを追加することを考え始めた場合は、設計を真剣に再考し、継承が依然として適切なツールであるかどうかを確認する必要があります。

他の良い答えに追加するには:

特に単一のクラスが作成する場合は、クラス階層を自動的に作成しないでください。多くのことは、特殊化(サブクラス)の提供としてではなく、一般的なケースでは属性を介してより適切にモデル化されます。

明らかに、あなたの例では、UniqueSedanMethodCalcAdditionalSeatsAddedが意図されています。ただし、この不自然な例を見ると、すべてのVehiclesにシートシート数や貨物容量などが必要であると主張するかもしれません。これらはSedanSUVに固有ではなく、特殊化とクラス階層の使用そのため、ここでは時期尚早です。このクラス階層を利用する任意の聴衆は、図解された(考案されたとしても)専門化を妨げる可能性が高いです。

ドメインに車両の「タイプ」があるからといって、自動的にOOP "is-a"メカニズム(継承)がドメインオブジェクトのモデル化に最適なアプローチであるとは限りません。車両のボディスタイルが情報が必要であり、継承を利用してボディスタイルを取得するのではなく、属性として提供することもできます。

OOPクラス階層が示される可能性がある場合にクライアントに通知するコード臭いがあります(たとえば、クライアントは、さまざまなケースが異なる特別な動作を行ういくつかの属性でswitchステートメントを使用しています)。

ティーチングドメインの別の例では、教師をPersonのサブクラスとして、生徒をPersonのサブクラスとしてモデリングする場合があります。ただし、Teacher&Studentはis-a関係というよりは一時的な役割です。ここでは、継承よりも構成が示されています。このクラス階層例には、実際には論理エラーがあります。これは、コースを受講する教師は異なるIDを想定する必要があるためです。一方、構成の下では、同じPersonが教師と生徒の両方になる場合があります。

クラス階層を削減すると、永続化も簡素化されます(それがアプリケーションの一部である場合)。

上記はすべて、モデリングの最初の選択肢として継承を使用しない理由です。

DRYコードと将来の保守性は、移植性とスケーラビリティのための疎結合のために犠牲にする必要がありますか?

継承を適切に使用すると、コードの再利用(DRY)のメカニズムが提供されます。ただし、これは簡単に悪用され、正反対の原因になる可能性があります(コードの重複)。たとえば、Sedanが座席数メソッドを導入しているのに、SUVCalcAdditionalSeatsAdded()を持っている場合、一般的なケースで座席数を見つけるには、クライアントが車両タイプを決定し、適切なメソッドを呼び出す必要があります。 SUVの場合は、追加の計算を実行します(追加のシートに追加)。 (これは、シート数が一般的なケースのインターフェースとして提供された可能性がある場合です。)


ここでの一般的な概念は、プログラミング言語のプリミティブコンストラクトと機能をプログラミング用のツールとして、抽象化を作成するためのツールと見なすことで、プログラミング/コーディングとドメインモデリング—これは、プログラミング言語の基本的な構造と機能をドメインモデリング(ツール)として見るのではなく、.

つまり、特定のフレーバー(任意の言語での継承および基本オブジェクト機能の)は、ドメインモデリングツールではありません。それは、ドメインをモデル化する抽象化をコーディングするためのツールです。

4
Erik Eidt

Driveは、車の種類に関係なく、同じであってはなりませんが、必要に応じて、メソッドを完全にオーバーライドできます(オーバーライドで必要な場合は、デフォルトの実装を使用します)。

私は完全に同意しません。 Driveをオーバーライドしてすべての基本動作を削除すると、LSPに違反することになります。

私は個人的にあなたの推論を完全に理解していません。しかし、私があなたの例で継承を合成に置き換えなければならないなら、それは非常に簡単でしょう。最初に、一般的なVehicle動作のすべてを非仮想メソッドに移動し、具体的な車両によって実装される抽象メソッドのみを残します。上で述べたように、私はこの一般的なVehicle動作が変更されることを期待しないため、これらすべてのメソッドを非仮想にすることは大したことではありません。残りの抽象メソッドは、インターフェイスを定義します。これは、具体的な車両が提供するものと期待しています。それでは、これらの抽象メソッドを個別のインターフェースに分割しないでください。それをIVehicleStrategyと呼びましょう。その後、コンクリートの乗り物はVehicleから継承されなくなりますが、代わりにIVehicleStrategyを実装します。彼らはまだ独自のメソッドとプロパティを持つことができます。そして、なぜそれを「戦略」と呼んだのですか?まあ、それは 戦略的パターン だからです。

2
Euphoric

私は「継承よりも構成」の擁護者です。メンテナンス可能なコードを書こうとしているのですか?私の経験では、「インターフェース」と「実装」のみを使用すると、非常に保守可能な設計が得られます。継承を混合しない非常に良い理由もあります。それは全体をより複雑にします。保守性はとりわけ単純さから生じるため、複雑さは保守性を損ないます。

特定のVehicleの例では、コードからデータを抽出するのが良い方法だと思います。それは正確に何を伴いますか?車両とギアシステムに関するすべてのデータを含むデータソースをセットアップします。あなたが残された唯一の実装は、単にデータを読み取ることによって具体的な自動車の1つを仮定するだけです。この方法で少ないコードを記述しただけでなく、数分簡単になりました。次に、特定の方法を必要とするVehicleの実装がないように、設計をやり直します。 「特定の方法」はLSP(SOLIDのL)に違反しており、全体的な保守性に悪影響を及ぼします。

このソリューションには、パフォーマンスに悪影響を与える可能性のあるわずかな欠点があります。私のように、保守性をもっと気にしているなら、それは問題ではありませんが。

1
RabbitBones22

SOLID原則OOP設計(および一般的なOOPアドバイス))によると、インターフェースにコーディングして使用しないでください継承

「インターフェースへのコード」=実際にはSOLIDの「D」の原則であり、実際には、モジュールは具体化ではなく抽象化に依存するべきであると述べています。

「継承を使用しない」=ナンセンスです。SOLIDの「L」の原則により、継承を正しく使用する方法が実際に示されます。

基本的に、あなたが与えた車の例は、どのシナリオが継承を必要とし、どのシナリオがインターフェースの実装を必要とするかを説明するには不十分です。

実際には、セダンは「車両」であると述べることができます。あなたのソフトウェアでは、あなたは実際のセダンを持っていません、あなたはそれのコード表現を持っています。次に、セダンのコード表現が車両の「a」コード表現であるかどうかを判断する必要があります。そうであれば、継承を安全に使用できます。これはプロジェクトのニーズに依存し、禁止されていません。

コードを再利用したい場合で、2つのクラス間に明示的な "is a"依存関係がない場合は、コンポジションを使用する必要があります。コード行を複製する必要はありません。

要約すると、これらのアプローチはすべて使用できます。これは、ニーズと、オブジェクトを正しくモデル化する方法によって異なります。さらに、SOLID原則のすべてを確認し、それらのすべてを理解していることを確認してください: https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)

0
Emerson Cardoso

私を困らせることが1つあります。抽象クラスには、子クラスによって設定できるプロパティがあります。なぜこれが悪いのですか? ドローバック の数があり、最も顕著で実用的なのは 脆弱な基本クラスの問題 です。簡単に言えば、オーバーライドされたメソッドは、親クラスが予期しないことを実行できます。したがって、最も基本的なOOP原則に違反しています- encapsulation です。継承を使用するときの私の個人的な経験則は、子クラスが親の状態を変更できないことです) 。

だから、あなたの質問:

「インターフェイスへのコードで継承を使用しない」という概念や依存関係の逆転の原則を誤解していますか?

そして

「インターフェイスへのコード」、依存関係の逆転、およびより良いソリューションを認識せず、継承を正しく利用していないと誤解していない場合、継承は{ここに否定および/または意味を表す形容詞を挿入}と見なされますか?私が考慮していない欠点はありますか?

ドメインが他の種類の車両を示唆していない場合、私はあなたのデザインは素晴らしいと思います(私が上で書いたものを覚えておいてください)。現在、あなたの行動は本来あるべき場所にあります。サービスクラスを導入すると、設計が悪化します。急いで インターフェースを導入 してください。まず、は、ドメイン分解が適切であることを自動的に保証しません。次に、 つのルール を思い出してください。一般的に人々は未来を予測するのが苦手です。第三に、 ドメインを分解する 最初に-コードの最初の行を書くよりもずっと早くソフトウェアが起動します。

そして、用語全体についてです。元々、継承は 再利用のメカニズム として実装されていました。しかし、継承の実装は、私が知っているすべての言語での サブタイピング の実装と同じであることは重要です。それにもかかわらず、それらはほとんど常に同義語として使用され、いずれかを使用する動機付けの力は大きく異なりました。

最後に、実装。ベース抽象クラスに多くのメソッドを実装していることがよくあります。その後、それらのほとんどすべてを小さな値オブジェクトに移動できることに気付き始めました。それで私はあなたのコードを実装する方法です(php :)申し訳ありません):

abstract class Vehicle
{
    public function drive()
    {
        if ($this->readyToDrive()) {
            // absolutely the same for all kinds of vehicle
        }
    }

    abstract protected function readyToDrive();
}

class Sedan extends Vehicle
{
    private $gear;
    private $ignition;
    private $tank;
    private $seatBelts;

    public function __construct(IGear $gear, IIgnition $ignition, Itank $tank, ISeatBelts $seatBelts)
    {
        $this->gear = $gear;
        $this->ignition = $ignition;
        $this->tank = $tank;
        $this->seatBelts = $seatBelts;
    }

    protected function readyToDrive()
    {
        return $this->gear->isDrive() && $this->ignition->hasKey() && $this->tank->isEnoughFuel() && $this->seatBelts->areOn();
    }
}
0
Zapadlo