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?
quelle
Antworten:
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()
inEmployee
sein solltevirtual
in Ihrem ersten Beispiel.Jetzt
do_work()
kann nicht überschrieben werden. Wenn Sie es erweitern möchten, müssen Sie es tun, überon_do_work()
das Siedo_work()
die Kontrolle haben.Dies kann natürlich auch mit der Schnittstelle aus Ihrem zweiten Beispiel verwendet werden, wenn diese
Employee
erweitert 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.quelle
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.
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.
Dann sollte Joe Coder das bekommen, was er verdient, wenn er fehlgeschlagene Unit-Tests ignoriert. Er hat diese Klasse getestet, nicht wahr? :) :)
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.
quelle
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 IEmployee
den gleichen Vorteil erzielen. Es gibt zahlreiche Möglichkeiten, mit solchen Dingen umzugehen.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.
quelle
get_name
. Alle Ihre vorgeschlagenen Implementierungen würden dieselbe Implementierung von teilenget_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.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.
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
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.
quelle
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.
quelle