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


10

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

インターフェースの問題は、それがデフォルトの実装を持つことができないことです。これは、ありふれたプロパティの苦痛であり、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 ++では純粋なvirtualであるメソッドを除いて、メソッドのオーバーライドが許可されていないと想像してください

したがって、ケース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()を呼び出すのを忘れた場合はどうなるでしょうか。


質問:

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

  • あるケース3よりも優れたケース2は、それはDRYに準拠しているため?


2
...抽象クラスを再発明していますか?
ZJR

回答:


10

スーパークラスの呼び出しを忘れる問題を解決する1つの方法は、コントロールをスーパークラスに戻すことです。私はあなたの最初の例を再調整して、その方法を示しました(そしてコンパイルしました;))。ああ、私はまた、仮定do_work()では、Employeeすることになった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()

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


3
そして、それが「テンプレートメソッド」として知られているデザインパターンです(en.wikipedia.org/wiki/Template_method_pattern)。
ジョリスティマーマンズ

はい、これはケース3に準拠しています。これは有望に見えます。詳しく調べます。また、これはある種のイベントシステムです。この「パターン」に名前はありますか?
MustafaM 2012

@MadKeithVこれは「テンプレートメソッド」ですか?
MustafaM 2012

@illmath-はい、これは非仮想のパブリックメソッドであり、実装の詳細の一部を仮想の保護された/プライベートのメソッドに委譲します。
Joris Timmermans、2012

@illmath以前はテンプレートメソッドとは考えていませんでしたが、基本的な例の1つだと思います。私はあなたが読みたいと思うかもしれないこの記事を見つけました。著者がそれ自体の名前に値すると信じています:非仮想インターフェースイディオム
Gyan別名Gary Buyn

1

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

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

一方、SOLIDは、すべてのクラスにインターフェースが必要であることを通知します。

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

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

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

次に、Joe Coderは、失敗した単体テストを無視するに値するものを取得する必要があります。彼はこのクラスをテストしましたね?:)

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

1つのサイズですべてに対応できるわけではありません。どちらが優れているかを知ることは不可能です。一方が他方よりもうまく適合するいくつかのケースがあります。

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


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


コメントをありがとう。目的をより明確にするために、ケース3を更新しました。
MustafaM

1
ここでは-1にしなければならない。すべてのインターフェースが純粋でなければならない、またはすべてのクラスがインターフェースから継承されるべきであると言う理由はまったくありません。
DeadMG

@DeadMG ISP
BЈовић

@VJovic:SOLIDと「すべてがインターフェースから継承する必要がある」との間には大きな違いがあります。
DeadMG

「1つのサイズではすべてに適合しない」および「いくつかのデザインパターンを学ぶ」は正しい-残りの回答は、1つのサイズではすべてに適合しないというあなた自身の提案に違反しています。
ジョリスティマーマンズ

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 ++のことであり、標準委員会がこれほどまでにこのような変更を実装することは決してありません。


「異なる実装はありません」と言います。でもやるよ。私はEmployeeのNurse実装を行っていますが、後で他の実装(Doctor、Janitorなど)を実装することもあります。ケース3を更新して、意味をより明確にしました。
MustafaM

@illmath:しかし、の他の実装はありませんget_name。提案されたすべての実装は、の同じ実装を共有しますget_name。その上、私が言ったように、選択する理由はありません、あなたは両方を持つことができます。また、ケース3はまったく価値がありません。非純粋な仮想を上書きできるので、できない仮想設計は忘れてください。
DeadMG

インターフェースはC ++でデフォルト実装を持つことができるだけでなく、デフォルト実装を持つことができ、それでも抽象的です!つまり、仮想vo​​id IMethod()= 0 {std :: cout << "Ni!" << std :: endl; }
Joris Timmermans、

@MadKeithV:インラインで定義できるとは思いませんが、ポイントは同じです。
DeadMG

@MadKeith:Visual Studioが標準C ++の特に正確な表現であったかのように。
DeadMG

0

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

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

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

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

質問を編集

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

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


0

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

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