web-dev-qa-db-ja.com

インターフェイスと継承:両方の長所?

私はインターフェースを「発見」し、それらを愛するようになりました。インターフェースの優れた点は、それがコントラクトであり、そのコントラクトを実行するオブジェクトは、そのインターフェースが必要な場所であればどこでも使用できることです。

インターフェイスの問題は、それがデフォルトの実装を持つことができないことです。これは、ありふれたプロパティの苦痛であり、DRYを無効にします。これは、実装とシステムの分離を維持するので、これも良い方法です。一方、継承はより緊密な結合を維持し、カプセル化を壊す可能性があります。

ケース1(プライベートメンバーによる継承、適切なカプセル化、密結合)

class Employee
{
int money_earned;
string name;

public:
 void do_work(){money_earned++;};
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work. Oops, can't update money_earned. Unaware I have to call superclass' do_work()*/);

};

void HireNurse(Nurse *n)
{
   nurse->do_work();
)

ケース2(インターフェースのみ)

class IEmployee
{
     virtual void do_work()=0;
     virtual string get_name()=0;
};

//class Nurse implements IEmployee.
//But now, for each employee, must repeat the get_name() implementation,
//and add a name member string, which breaks DRY.

ケース3:(両方の長所?)

ケース1に似ています。ただし、(仮想的に)C++であることを想像してくださいメソッドのオーバーライドが許可されていませんexcept純粋な仮想であるメソッド。

したがって、ケース1では、do_work()をオーバーライドするとコンパイル時エラーが発生します。これを修正するには、do_work()を純粋仮想として設定し、別のメソッドincrement_money_earned()を追加します。例として:

class Employee
{
int money_earned;
string name;

public:
 virtual void do_work()=0;
 void increment_money_earned(money_earned++;);
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work*/ increment_money_earned(); ); .
};

しかし、これにも問題があります。 Joe Coderが今から3か月後にDoctor Employeeを作成したが、do_work()でincrement_money_earned()を呼び出すのを忘れた場合はどうなりますか?


質問:

  • ケースケース1より優れていますか?それは「カプセル化の向上」または「疎結合性が高い」ためか、それとも他の理由によるのでしょうか。

  • ケースケース2より優れているので、DRYに準拠していますか?

10
MustafaM

スーパークラスの呼び出しを忘れる問題を解決する1つの方法は、スーパークラスに制御を戻すことです。私はあなたの最初の例を再調整して、その方法を示しました(そしてコンパイルしました;))。ああ、私はまた、最初の例ではEmployeedo_work()virtualであると想定されていました。

_#include <string>

using namespace std;

class Employee
{
    int money_earned;
    string name;
    virtual void on_do_work() {}

    public:
        void do_work() { money_earned++; on_do_work(); }
        string get_name() { return name; }
};

class Nurse : public Employee
{
    void on_do_work() { /* do more work. Oh, and I don't have to call do_work()! */ }
};

void HireNurse(Nurse* nurse)
{
    nurse->do_work();
}
_

現在、do_work()はオーバーライドできません。それを拡張したい場合は、on_do_work()が制御するdo_work()を介して行う必要があります。

もちろん、これは、2番目の例のインターフェースでも、Employeeが拡張する場合に使用できます。だから、私があなたを正しく理解していれば、それはこのケース3を作ると思いますが、架空のC++を使用する必要はありません! DRYで、カプセル化が強力です。

10

インターフェイスの問題は、それがデフォルトの実装を持つことができないことです。これは、ありふれたプロパティの苦痛であり、DRYを無効にします。

私の意見では、インターフェイスにはデフォルトの実装なしで、純粋なメソッドのみが含まれている必要があります。 DRYの原則は、インターフェースがエンティティにアクセスする方法を示しているため、どのような方法でも壊れません。参照のためだけに、DRY説明を参照しています ここ
「すべての知識は、システム内で単一の明確で信頼できる表現を持つ必要があります。」

一方、 [〜#〜] solid [〜#〜] は、すべてのクラスにインターフェイスが必要であることを示しています。

ケース3はケース1より優れていますか?それは「カプセル化の向上」または「疎結合性が高い」ためか、それとも他の理由によるのでしょうか。

いいえ、ケース3はケース1より優れているわけではありません。デフォルトの実装が必要な場合は、そうしてください。純粋なメソッドが必要な場合は、それを使用してください。

Joe Coderが今から3か月後にDoctor Employeeを作成したが、do_work()でincrement_money_earned()を呼び出すのを忘れた場合はどうなりますか?

次に、Joe Coderは、失敗したユニットテストを無視するのにふさわしいものを取得する必要があります。彼はこのクラスをテストしましたね? :)

40,000行のコードを含む可能性があるソフトウェアプロジェクトに最適なケースはどれですか。

1つのサイズですべてに対応できるわけではありません。どちらが良いかはわかりません。一方が他方よりもうまくフィットする場合があります。

たぶん、あなたはあなた自身のいくつかを発明しようとする代わりに、いくつかの デザインパターン を学ぶべきです。


非仮想インターフェイス デザインパターンを探していることに気づきました。これは、ケース3のクラスがどのようなものかということです。

1
BЈовић

ケース3はケース1より優れていますか?それは「カプセル化の向上」または「疎結合性が高い」ためか、それとも他の理由によるのでしょうか。

私があなたの実装で見るものから、あなたのCase実装は、後で派生クラスで変更できる純粋な仮想メソッドを実装できる抽象クラスを必要とします。 ケース派生クラスは必要に応じてdo_workの実装を変更でき、すべての派生インスタンスは基本的に基本抽象型に属します。

40,000行のコードが含まれる可能性があるソフトウェアプロジェクトに最適なケースはどれですか。

それは純粋にあなたの実装設計とあなたが達成したい目的に依存すると私は言うでしょう。抽象クラスとインターフェイスは、解決する必要がある問題に基づいて実装されます。

質問を編集

Joe Coderが今から3か月後にDoctor Employeeを作成したが、do_work()でincrement_money_earned()を呼び出すのを忘れた場合はどうなりますか?

ユニットテストを実行して、各クラスが期待される動作を確認するかどうかを確認できます。したがって、適切な単体テストが適用されれば、Joe Coderが新しいクラスを実装するときにバグを防ぐことができます。

0

インターフェイスは、C++のデフォルト実装を持つことができます。関数のデフォルトの実装が他の仮想メンバー(および引数)にのみ依存するわけではないため、いかなる種類の結合も増加させないということは何もありません。

ケース2の場合、DRYはここで置き換えられます。異なる実装からの変更からプログラムを保護するためにカプセル化が存在しますが、この場合、異なる実装はありません。したがって、YAGNIカプセル化です。

実際、ランタイムインターフェイスは、通常、対応するコンパイル時のインターフェイスよりも劣ると考えられています。コンパイル時のケースでは、同じバンドルに両方ケース1 およびケース2を含めることができます。他にも多くの利点があります。または実行時でも、単にEmployee : public IEmployee実質的に同じ利点。このようなことに対処する方法は数多くあります。

Case 3: (best of both worlds?)

Similar to Case 1. However, imagine that (hypothetically)

読書をやめました。 YAGNI。 C++はC++とは何か、そして標準委員会がこれほどまでにこのような変更を実装することは決してありません。

0
DeadMG

インターフェースを使用すると、DRYが壊れるのは、各実装が互いに重複している場合のみです。両方のインターフェースand継承を適用​​することでこのジレンマを解決できますが、複数のクラスに同じインターフェースを実装したい場合がありますが、各クラスの動作は異なります。これにより、DRYの原則が維持されます。説明した3つのアプローチのいずれを使用するかは、特定の状況に一致する最適な手法を適用するために必要な選択に加えます。その一方で、おそらく時間が経つにつれて、インターフェイスをさらに使用し、繰り返しを削除したい場所にのみ継承を適用​​することに気付くでしょう。これは継承のonly理由であると言いますが、継承の使用を最小限にして、オプションを見つけた場合に開いておくことができるようにすることをお勧めします設計を後で変更する必要があり、変更による影響による子孫クラスへの影響を最小限にしたい場合eは親クラスで紹介します。

0
S.Robins