Schnittstelle und Vererbung: Das Beste aus beiden Welten?


10

Ich 'entdeckte' Schnittstellen und fing an, sie zu lieben. Das Schöne an einer Schnittstelle ist, dass es sich um einen Vertrag handelt und jedes Objekt, das diesen Vertrag erfüllt, überall dort verwendet werden kann, wo diese Schnittstelle erforderlich ist.

Das Problem mit einer Schnittstelle ist, dass sie keine Standardimplementierung haben kann, was für alltägliche Eigenschaften schmerzhaft ist und DRY besiegt. Dies ist auch gut, weil dadurch die Implementierung und das System entkoppelt bleiben. Die Vererbung hingegen hält eine engere Kopplung aufrecht und kann die Einkapselung unterbrechen.

Fall 1 (Vererbung mit privaten Mitgliedern, gute Kapselung, eng gekoppelt)

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

Fall 2 (nur eine Schnittstelle)

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.

Fall 3: (das Beste aus beiden Welten?)

Ähnlich wie in Fall 1 . Stellen Sie sich jedoch vor, dass C ++ (hypothetisch) keine überschreibenden Methoden außer den rein virtuellen Methoden zuließ .

In Fall 1 würde das Überschreiben von do_work () einen Fehler bei der Kompilierung verursachen. Um dies zu beheben, setzen wir do_work () als rein virtuell und fügen eine separate Methode increment_money_earned () hinzu. Als Beispiel:

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

Aber auch das hat Probleme. Was ist, wenn Joe Coder in 3 Monaten einen Doctor Employee erstellt, aber vergisst, increment_money_earned () in do_work () aufzurufen?


Die Frage:

  • Ist Fall 3 Fall 1 überlegen ? Liegt es an einer „besseren Einkapselung“ oder einer „lockereren Kopplung“ oder an einem anderen Grund?

  • Ist Fall 3 Fall 2 überlegen, weil er mit DRY übereinstimmt?


2
... erfinden Sie abstrakte Klassen neu oder was?
ZJR

Antworten:


10

Eine Möglichkeit, das Problem des Vergessens, die Oberklasse anzurufen, zu lösen, besteht darin, die Kontrolle an die Oberklasse zurückzugeben! Ich habe Ihr erstes Beispiel neu gestartet, um zu zeigen, wie (und es kompiliert hat;)). Oh, ich nehme auch an, dass do_work()in Employeesein sollte virtualin Ihrem ersten Beispiel.

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

Jetzt do_work()kann nicht überschrieben werden. Wenn Sie es erweitern möchten, müssen Sie es tun, über on_do_work()das Sie do_work()die Kontrolle haben.

Dies kann natürlich auch mit der Schnittstelle aus Ihrem zweiten Beispiel verwendet werden, wenn diese Employeeerweitert wird. Wenn ich Sie also richtig verstehe, denke ich, dass dies den Fall 3 ergibt, ohne jedoch hypothetisches C ++ verwenden zu müssen! Es ist trocken und hat eine starke Einkapselung.


3
Und das ist das Entwurfsmuster, das als "Vorlagenmethode" bekannt ist ( en.wikipedia.org/wiki/Template_method_pattern ).
Joris Timmermans

Ja, dies ist Fall 3-konform. Das sieht vielversprechend aus. Wird im Detail untersuchen. Dies ist auch eine Art Ereignissystem. Gibt es einen Namen für dieses "Muster"?
MustafaM

@MadKeithV Sind Sie sicher, dass dies die 'Vorlagenmethode' ist?
MustafaM

@illmath - Ja, es ist eine nicht virtuelle öffentliche Methode, die Teile ihrer Implementierungsdetails an virtuelle geschützte / private Methoden delegiert.
Joris Timmermans

@illmath Ich hatte es vorher nicht als Vorlagenmethode angesehen, aber ich glaube, es ist ein grundlegendes Beispiel dafür. Ich habe gerade diesen Artikel gefunden, den Sie vielleicht lesen möchten, wo der Autor glaubt, dass er seinen eigenen Namen verdient: Nicht-virtuelle Schnittstellen-Sprache
Gyan alias Gary Buyn

1

Das Problem mit einer Schnittstelle ist, dass sie keine Standardimplementierung haben kann, was für alltägliche Eigenschaften schmerzhaft ist und DRY besiegt.

Meiner Meinung nach sollten Schnittstellen nur reine Methoden haben - ohne Standardimplementierung. Das DRY-Prinzip wird dadurch in keiner Weise verletzt, da Schnittstellen zeigen, wie auf eine Entität zugegriffen werden kann. Nur als Referenz betrachte ich hier die DRY-Erklärung :
"Jedes Wissen muss eine einzige, eindeutige, maßgebliche Darstellung innerhalb eines Systems haben."

Andererseits sagt Ihnen die SOLID , dass jede Klasse eine Schnittstelle haben sollte.

Ist Fall 3 Fall 1 überlegen? Liegt es an einer „besseren Einkapselung“ oder einer „lockereren Kopplung“ oder an einem anderen Grund?

Nein, Fall 3 ist Fall 1 nicht überlegen. Sie müssen sich entscheiden. Wenn Sie eine Standardimplementierung wünschen, tun Sie dies. Wenn Sie eine reine Methode wollen, dann gehen Sie damit.

Was ist, wenn Joe Coder in 3 Monaten einen Doctor Employee erstellt, aber vergisst, increment_money_earned () in do_work () aufzurufen?

Dann sollte Joe Coder das bekommen, was er verdient, wenn er fehlgeschlagene Unit-Tests ignoriert. Er hat diese Klasse getestet, nicht wahr? :) :)

Welcher Fall eignet sich am besten für ein Softwareprojekt mit möglicherweise 40.000 Codezeilen?

Eine Größe passt nicht für alle. Es ist unmöglich zu sagen, welches besser ist. Es gibt einige Fälle, in denen einer besser passt als der andere.

Vielleicht sollten Sie einige Designmuster lernen , anstatt zu versuchen, einige eigene zu erfinden.


Ich habe gerade festgestellt, dass Sie nach einem nicht virtuellen Schnittstellen- Entwurfsmuster suchen , denn so sieht Ihre Fall-3-Klasse aus.


Danke für den Kommentar. Ich habe Fall 3 aktualisiert, um meine Absicht klarer zu machen.
MustafaM

1
Ich muss dich hier -1. Es gibt überhaupt keinen Grund zu sagen, dass alle Schnittstellen rein sein sollten oder dass alle Klassen von einer Schnittstelle erben sollten.
DeadMG

@DeadMG ISP
BЈовић

@VJovic: Es gibt einen großen Unterschied zwischen SOLID und "Alles muss von einer Schnittstelle erben".
DeadMG

"Eine Größe passt nicht für alle" und "einige Designmuster lernen" sind korrekt - der Rest Ihrer Antwort verstößt gegen Ihren eigenen Vorschlag, dass eine Größe nicht für alle passt.
Joris Timmermans

0

Schnittstellen können Standardimplementierungen in C ++ haben. Es gibt keine Anhaltspunkte dafür, dass eine Standardimplementierung einer Funktion nicht nur von anderen virtuellen Mitgliedern (und Argumenten) abhängt, sondern auch keine Kopplung erhöht.

Für Fall 2 ersetzt DRY hier. Die Kapselung schützt Ihr Programm vor Änderungen vor unterschiedlichen Implementierungen. In diesem Fall haben Sie jedoch keine unterschiedlichen Implementierungen. Also YAGNI-Kapselung.

Tatsächlich werden Laufzeitschnittstellen normalerweise als schlechter als ihre Kompilierungszeitäquivalente angesehen. Im Fall der Kompilierungszeit können Sie sowohl Fall 1 als auch Fall 2 im selben Bundle haben - ganz zu schweigen von den zahlreichen anderen Vorteilen. Oder sogar zur Laufzeit können Sie einfach Employee : public IEmployeeden gleichen Vorteil erzielen. Es gibt zahlreiche Möglichkeiten, mit solchen Dingen umzugehen.

Case 3: (best of both worlds?)

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

Ich hörte auf zu lesen. YAGNI. C ++ ist das, was C ++ ist, und das Standards Committee wird eine solche Änderung aus hervorragenden Gründen niemals umsetzen.


Sie sagen "Sie haben keine unterschiedlichen Implementierungen". Aber ich tue. Ich habe eine Krankenschwester-Implementierung des Mitarbeiters und möglicherweise später andere Implementierungen (ein Arzt, ein Hausmeister usw.). Ich habe Fall 3 aktualisiert, um klarer zu machen, was ich meinte.
MustafaM

@illmath: Aber du hast keine anderen Implementierungen von get_name. Alle Ihre vorgeschlagenen Implementierungen würden dieselbe Implementierung von teilen get_name. Außerdem gibt es, wie gesagt, keinen Grund zu wählen, Sie können beides haben. Auch Fall 3 ist absolut wertlos. Sie können nicht reine Virtuals überschreiben. Vergessen Sie also ein Design, bei dem dies nicht möglich ist.
DeadMG

Schnittstellen können nicht nur Standardimplementierungen in C ++ haben, sie können auch Standardimplementierungen haben und dennoch abstrakt sein! dh virtuelle Leere IMethod () = 0 {std :: cout << "Ni!" << std :: endl; }
Joris Timmermans

@ MadKeithV: Ich glaube nicht, dass Sie sie inline definieren können, aber der Punkt ist immer noch der gleiche.
DeadMG

@ MadKeith: Als ob Visual Studio jemals eine besonders genaue Darstellung von Standard C ++ gewesen wäre.
DeadMG

0

Ist Fall 3 Fall 1 überlegen? Liegt es an einer „besseren Einkapselung“ oder einer „lockereren Kopplung“ oder an einem anderen Grund?

Nach dem, was ich in Ihrer Implementierung sehe, erfordert Ihre Case 3- Implementierung eine abstrakte Klasse, die reine virtuelle Methoden implementieren kann, die später in der abgeleiteten Klasse geändert werden können. Fall 3 wäre besser, da die abgeleitete Klasse die Implementierung von do_work nach Bedarf ändern kann und alle abgeleiteten Instanzen grundsätzlich zum abstrakten Basistyp gehören würden.

Welcher Fall ist der beste für ein Softwareprojekt mit möglicherweise 40.000 Codezeilen?

Ich würde sagen, es hängt nur von Ihrem Implementierungsdesign und dem Ziel ab, das Sie erreichen möchten. Abstrakte Klasse und Schnittstellen werden basierend auf dem zu lösenden Problem implementiert.

Auf Frage bearbeiten

Was ist, wenn Joe Coder in 3 Monaten einen Doctor Employee erstellt, aber vergisst, increment_money_earned () in do_work () aufzurufen?

Unit-Tests können durchgeführt werden, um zu überprüfen, ob jede Klasse das erwartete Verhalten bestätigt. Wenn also geeignete Komponententests angewendet werden, können Fehler verhindert werden, wenn Joe Coder die neue Klasse implementiert.


0

Die Verwendung von Schnittstellen unterbricht DRY nur, wenn jede Implementierung ein Duplikat voneinander ist. Sie können dieses Dilemma lösen, indem Sie sowohl die Schnittstelle als auch die Vererbung anwenden. In einigen Fällen möchten Sie jedoch möglicherweise dieselbe Schnittstelle für eine Reihe von Klassen implementieren, das Verhalten in jeder der Klassen variieren, dies bleibt jedoch dem Prinzip treu von DRY. Ob Sie sich für einen der drei von Ihnen beschriebenen Ansätze entscheiden, hängt von den Entscheidungen ab, die Sie treffen müssen, um die beste Technik für eine bestimmte Situation anzuwenden. Auf der anderen Seite werden Sie wahrscheinlich feststellen, dass Sie im Laufe der Zeit mehr Schnittstellen verwenden und die Vererbung nur dort anwenden, wo Sie Wiederholungen entfernen möchten. Das heißt nicht, dass dies der einzige ist Grund für die Vererbung, aber es ist besser, die Verwendung der Vererbung zu minimieren, damit Sie Ihre Optionen offen halten können, wenn Sie feststellen, dass sich Ihr Design später ändern muss, und wenn Sie die Auswirkungen einer Änderung auf Nachkommenklassen minimieren möchten würde in einer Elternklasse vorstellen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.