Ein Kollege von mir hat eine Faustregel für die Auswahl zwischen der Erstellung einer Basisklasse oder einer Schnittstelle entwickelt.
Er sagt:
Stellen Sie sich jede neue Methode vor, die Sie implementieren möchten. Für jeden von ihnen, bedenken Sie: Diese Methode wird von mehr als einer Klasse implementiert werden , in genau dieser Form ohne jede Veränderung? Wenn die Antwort "Ja" lautet, erstellen Sie eine Basisklasse. Erstellen Sie in jeder anderen Situation eine Schnittstelle.
Zum Beispiel:
Betrachten Sie die Klassen
cat
unddog
, die die Klasse erweiternmammal
und eine einzige Methode habenpet()
. Wir fügen dann die Klasse hinzualligator
, die nichts erweitert und eine einzige Methode hatslither()
.Jetzt möchten wir
eat()
allen eine Methode hinzufügen .Wenn die Implementierung der
eat()
Methode für und genau gleichcat
ist , sollten wir eine Basisklasse (z. B. ) erstellen , die diese Methode implementiert.dog
alligator
animal
Wenn sich die Implementierung jedoch im
alligator
geringsten unterscheidet, sollten wir eineIEat
Schnittstelle erstellenmammal
undalligator
diese erstellen und implementieren.
Er besteht darauf, dass diese Methode alle Fälle abdeckt, aber es scheint mir eine übermäßige Vereinfachung zu sein.
Lohnt es sich, diese Faustregel zu befolgen?
quelle
alligator
Die Implementierung voneat
unterscheidet sich natürlich darin, dass siecat
unddog
als Parameter akzeptiert .is a
und Schnittstellen sindacts like
oderis
. Also einis a
Hundesäugetier undacts like
ein Esser. Dies würde uns sagen, dass Säugetiere eine Klasse und Esser eine Schnittstelle sein sollten. Es war schon immer eine sehr hilfreiche Anleitung. Nebenbemerkung: Ein Beispielis
wäreThe cake is eatable
oderThe book is writable
.Antworten:
Ich denke nicht, dass dies eine gute Faustregel ist. Wenn Sie sich Gedanken über die Wiederverwendung von Code machen, können Sie eine
PetEatingBehavior
Rolle implementieren, die darin besteht, die Essfunktionen für Katzen und Hunde zu definieren. Dann können SieIEat
Code gemeinsam wiederverwenden.In diesen Tagen sehe ich immer weniger Gründe, Vererbung zu verwenden. Ein großer Vorteil ist die Benutzerfreundlichkeit. Nehmen Sie ein GUI-Framework. Eine beliebte Methode zum Entwerfen einer API hierfür besteht darin, eine große Basisklasse verfügbar zu machen und zu dokumentieren, welche Methoden der Benutzer überschreiben kann. So können wir alle anderen komplexen Dinge ignorieren, die unsere Anwendung verwalten muss, da die Basisklasse eine "Standard" -Implementierung vorgibt. Aber wir können die Dinge trotzdem anpassen, indem wir die richtige Methode bei Bedarf neu implementieren.
Wenn Sie sich an die Benutzeroberfläche für Ihre API halten, hat Ihr Benutzer normalerweise mehr Arbeit zu erledigen, damit einfache Beispiele funktionieren. APIs mit Schnittstellen sind jedoch häufig weniger gekoppelt und aus dem einfachen Grund einfacher zu warten, da sie
IEat
weniger Informationen enthalten als dieMammal
Basisklasse. Die Verbraucher sind also auf einen schwächeren Vertrag angewiesen, Sie erhalten mehr Refactoring-Möglichkeiten usw.Um Rich Hickey zu zitieren: Einfach! = Einfach
quelle
PetEatingBehavior
Implementierung liefern ?PetEatingBehavior
ist wahrscheinlich der falsche. Ich kann Ihnen keine Implementierung geben, da dies nur ein Spielzeugbeispiel ist. Aber ich kann Refactoring-Schritte beschreiben: Es gibt einen Konstruktor, der alle Informationen verwendet, die von Katzen / Hunden verwendet werden, um dieeat
Methode zu definieren (Zahninstanz, Mageninstanz usw.). Es enthält nur den Code, der Katzen und Hunden gemeinsam ist, die in ihrereat
Methode verwendet werden. Sie müssen lediglich einPetEatingBehavior
Mitglied erstellen und die Aufrufe an dog.eat/cat.eat weiterleiten.Es ist eine anständige Faustregel, aber ich kenne einige Stellen, an denen ich dagegen verstoße.
Um fair zu sein, benutze ich viel mehr (abstrakte) Basisklassen als meine Kollegen. Ich mache das als defensive Programmierung.
Für mich (in vielen Sprachen) fügt eine abstrakte Basisklasse die Schlüsseleinschränkung hinzu, dass es möglicherweise nur eine Basisklasse gibt. Wenn Sie sich für die Verwendung einer Basisklasse anstelle einer Schnittstelle entscheiden, sagen Sie: "Dies ist die Kernfunktionalität der Klasse (und jedes Erben), und es ist unangemessen, sie ohne weiteres mit anderen Funktionen zu mischen." Schnittstellen sind das Gegenteil: "Dies ist ein Merkmal eines Objekts, nicht unbedingt seine Kernverantwortung".
Diese Art der Entwurfsauswahl erstellt implizite Anleitungen für Ihre Implementierer, wie sie Ihren Code verwenden sollen, und schreibt im weiteren Sinne ihren Code. Aufgrund ihrer größeren Auswirkung auf die Codebasis neige ich dazu, diese Dinge bei der Entwurfsentscheidung stärker zu berücksichtigen.
quelle
Das Problem bei der Vereinfachung Ihres Freundes besteht darin, dass es außerordentlich schwierig ist, festzustellen, ob eine Methode jemals geändert werden muss oder nicht. Es ist besser, eine Regel zu verwenden, die die Mentalität hinter Oberklassen und Schnittstellen anspricht.
Anstatt herauszufinden, was Sie tun sollten, indem Sie sich ansehen, was Ihrer Meinung nach die Funktionalität ist, schauen Sie sich die Hierarchie an, die Sie erstellen möchten.
So hat ein Professor von mir den Unterschied gelehrt:
Superklassen sollten sein
is a
und Schnittstellen sindacts like
oderis
. Also einis a
Hundesäugetier undacts like
ein Esser. Dies würde uns sagen, dass Säugetiere eine Klasse und Esser eine Schnittstelle sein sollten. Ein Beispielis
wäreThe cake is eatable
oderThe book is writable
(beideeatable
undwritable
Schnittstellen machen).Die Verwendung einer solchen Methodik ist relativ einfach und führt dazu, dass Sie nach der Struktur und den Konzepten codieren und nicht nach dem, was der Code Ihrer Meinung nach bewirken wird. Dies erleichtert die Wartung, die Lesbarkeit des Codes und die Erstellung des Designs.
Unabhängig davon, was der Code tatsächlich aussagt, könnten Sie in fünf Jahren zurückkehren und dieselbe Methode verwenden, um Ihre Schritte nachzuvollziehen und auf einfache Weise herauszufinden, wie Ihr Programm entworfen wurde und wie es interagiert.
Nur meine zwei Cent.
quelle
Auf Wunsch von exizt erweitere ich meinen Kommentar zu einer längeren Antwort.
Der größte Fehler, den die Leute machen, ist zu glauben, dass Schnittstellen einfach leere abstrakte Klassen sind. Eine Schnittstelle ist eine Möglichkeit für einen Programmierer zu sagen: "Es ist mir egal, was Sie mir geben, solange es dieser Konvention folgt."
Die .NET-Bibliothek dient als wunderbares Beispiel. Wenn Sie beispielsweise eine Funktion schreiben, die eine akzeptiert
IEnumerable<T>
, sagen Sie: "Es ist mir egal, wie Sie Ihre Daten speichern. Ich möchte nur wissen, dass ich eineforeach
Schleife verwenden kann.Dies führt zu einem sehr flexiblen Code. Um integriert zu werden, müssen Sie plötzlich nur noch die Regeln der vorhandenen Schnittstellen einhalten. Wenn die Implementierung der Schnittstelle schwierig oder verwirrend ist, ist dies möglicherweise ein Hinweis darauf, dass Sie versuchen, einen quadratischen Stift in ein rundes Loch zu schieben.
Aber dann stellt sich die Frage: "Was ist mit der Wiederverwendung von Code? Meine CS-Professoren sagten mir, dass die Vererbung die Lösung für alle Probleme bei der Wiederverwendung von Code ist und dass die Vererbung es Ihnen ermöglicht, einmal zu schreiben und überall zu verwenden, und Menangitis bei der Rettung von Waisenkindern heilen würde von den aufsteigenden Meeren und es würde keine Tränen mehr geben und weiter und weiter usw. usw. usw. "
Die Verwendung der Vererbung, nur weil Sie den Klang der Wörter "Wiederverwendung von Code" mögen, ist eine ziemlich schlechte Idee. Der Code-Styling-Leitfaden von Google macht diesen Punkt ziemlich präzise:
Um zu veranschaulichen, warum Vererbung nicht immer die Antwort ist, verwende ich eine Klasse namens MySpecialFileWriter †. Eine Person, die blindlings glaubt, dass Vererbung die Lösung für alle Probleme ist, würde argumentieren, dass Sie versuchen sollten, von zu erben
FileStream
, damit SieFileStream
den Code nicht duplizieren . Kluge Leute erkennen, dass das dumm ist. Sie sollten nur einFileStream
Objekt in Ihrer Klasse haben (entweder als lokale oder als Mitgliedsvariable) und dessen Funktionalität verwenden.Das
FileStream
Beispiel mag erfunden erscheinen, ist es aber nicht. Wenn Sie zwei Klassen haben, die beide dieselbe Schnittstelle auf genau dieselbe Weise implementieren, sollten Sie eine dritte Klasse haben, die jede duplizierte Operation kapselt. Ihr Ziel sollte es sein, Klassen zu schreiben, die in sich geschlossene wiederverwendbare Blöcke sind, die wie Legos zusammengestellt werden können.Dies bedeutet nicht, dass Vererbung um jeden Preis vermieden werden sollte. Es gibt viele Punkte zu beachten, und die meisten werden durch die Untersuchung der Frage "Zusammensetzung vs. Vererbung" abgedeckt. Unser eigener Stapelüberlauf hat einige gute Antworten zu diesem Thema.
Letztendlich fehlt den Gefühlen Ihres Mitarbeiters die Tiefe oder das Verständnis, die erforderlich sind, um eine fundierte Entscheidung zu treffen. Erforschen Sie das Thema und finden Sie es selbst heraus.
† Bei der Veranschaulichung der Vererbung verwendet jeder Tiere. Das ist nutzlos. In 11 Jahren Entwicklungszeit habe ich noch nie eine Klasse mit dem Namen geschrieben
Cat
, daher werde ich sie nicht als Beispiel verwenden.quelle
FileStream
)Beispiele machen selten einen Punkt, besonders nicht, wenn es um Autos oder Tiere geht. Die Art und Weise, wie ein Tier frisst (oder Nahrung verarbeitet), ist nicht wirklich an sein Erbe gebunden. Es gibt keine einzige Art zu essen, die für alle Tiere gilt. Dies hängt mehr von seinen physischen Fähigkeiten ab (sozusagen von der Art der Eingabe, Verarbeitung und Ausgabe). Säugetiere können beispielsweise Fleischfresser, Pflanzenfresser oder Allesfresser sein, während Vögel, Säugetiere und Fische Insekten fressen können. Dieses Verhalten kann mit bestimmten Mustern erfasst werden.
In diesem Fall sind Sie besser dran, wenn Sie Verhalten wie folgt abstrahieren:
Oder implementieren Sie ein Besuchermuster, bei dem
FoodProcessor
versucht wird, kompatible Tiere zu füttern (ICarnivore : IFoodEater
,MeatProcessingVisitor
). Der Gedanke an lebende Tiere kann Sie daran hindern, daran zu denken, ihre Verdauungsspur herauszureißen und durch eine generische zu ersetzen, aber Sie tun ihnen wirklich einen Gefallen.Dies macht Ihr Tier zu einer Basisklasse, und noch besser zu einer, die Sie nie wieder ändern müssen, wenn Sie eine neue Art von Futter und einen entsprechenden Verarbeiter hinzufügen, damit Sie sich auf die Dinge konzentrieren können, die diese bestimmte Tierklasse zu Ihnen machen Arbeiten an so einzigartig.
Also ja, Basisklassen haben ihren Platz, die Faustregel gilt (oft, nicht immer, da es sich um eine Faustregel handelt), aber suchen Sie weiter nach Verhalten, das Sie isolieren können.
quelle
IConsumer
Schnittstelle. Fleischfresser können Fleisch konsumieren, Pflanzenfresser Pflanzen und Pflanzen Sonnenlicht und Wasser.Eine Schnittstelle ist wirklich ein Vertrag. Es gibt keine Funktionalität. Die Implementierung muss vom Implementierer durchgeführt werden. Es gibt keine Wahl, da man den Vertrag umsetzen muss.
Abstrakte Klassen geben Implementierern die Wahl. Man kann sich für eine Methode entscheiden, die jeder implementieren muss, eine Methode mit Basisfunktionalität bereitstellen, die überschrieben werden kann, oder eine Basisfunktionalität bereitstellen, die alle Erben verwenden können.
Zum Beispiel:
Ich würde sagen, Ihre Faustregel könnte auch von einer Basisklasse behandelt werden. Insbesondere da viele Säugetiere wahrscheinlich dasselbe "essen", ist die Verwendung einer Basisklasse möglicherweise besser als eine Schnittstelle, da eine virtuelle Essmethode implementiert werden könnte, die die meisten Erben verwenden könnten, und dann die Ausnahmefälle überschreiben könnten.
Wenn nun die Definition von "Essen" von Tier zu Tier stark variiert, wäre vielleicht und Schnittstelle besser. Dies ist manchmal eine Grauzone und hängt von der individuellen Situation ab.
quelle
Nein.
Bei Schnittstellen geht es darum, wie die Methoden implementiert werden. Wenn Sie Ihre Entscheidung, wann Sie die Schnittstelle verwenden möchten, darauf stützen, wie die Methoden implementiert werden, dann machen Sie etwas falsch.
Für mich liegt der Unterschied zwischen Schnittstelle und Basisklasse darin, wo die Anforderungen stehen. Sie erstellen eine Schnittstelle, wenn für einen Code eine Klasse mit einer bestimmten API und einem impliziten Verhalten erforderlich ist, das sich aus dieser API ergibt. Sie können keine Schnittstelle ohne Code erstellen, der sie tatsächlich aufruft.
Auf der anderen Seite sind Basisklassen normalerweise als Möglichkeit zur Wiederverwendung von Code gedacht, sodass Sie sich selten mit Code beschäftigen, der diese Basisklasse aufruft, und nur die Suche nach Objekten berücksichtigen, zu denen diese Basisklasse gehört.
quelle
eat()
bei jedem Tier neu implementieren ? Wir können esmammal
umsetzen, nicht wahr? Ich verstehe jedoch Ihren Standpunkt zu den Anforderungen.Dies ist keine gute Faustregel, denn wenn Sie unterschiedliche Implementierungen wünschen, können Sie immer eine abstrakte Basisklasse mit einer abstrakten Methode haben. Jeder Erbe hat garantiert diese Methode, aber jeder definiert seine eigene Implementierung. Technisch gesehen könnte man eine abstrakte Klasse mit nur einer Reihe abstrakter Methoden haben, und das wäre im Grunde dasselbe wie eine Schnittstelle.
Basisklassen sind nützlich, wenn Sie eine tatsächliche Implementierung eines Status oder Verhaltens wünschen, das für mehrere Klassen verwendet werden kann. Wenn Sie mehrere Klassen mit identischen Feldern und Methoden mit identischen Implementierungen haben, können Sie diese wahrscheinlich in eine Basisklasse umgestalten. Sie müssen nur vorsichtig sein, wie Sie erben. Jedes vorhandene Feld oder jede vorhandene Methode in der Basisklasse muss im Erben sinnvoll sein, insbesondere wenn es als Basisklasse umgewandelt wird.
Schnittstellen sind nützlich, wenn Sie auf die gleiche Weise mit einer Reihe von Klassen interagieren müssen, die tatsächliche Implementierung jedoch nicht vorhersagen oder sich nicht darum kümmern können. Nehmen Sie zum Beispiel so etwas wie eine Sammlungsschnittstelle. Herr kennt nur alle unzähligen Möglichkeiten, wie sich jemand entscheiden könnte, eine Sammlung von Gegenständen zu implementieren. Verknüpfte Liste? Anordnungsliste? Stapel? Warteschlange? Diese SearchAndReplace-Methode kümmert sich nicht darum, sie muss nur etwas übergeben werden, das Add, Remove und GetEnumerator aufrufen kann.
quelle
Ich beobachte die Antworten auf diese Frage selbst, um zu sehen, was andere Leute zu sagen haben, aber ich werde drei Dinge sagen, über die ich gerne nachdenke:
Die Leute setzen viel zu viel Vertrauen in Schnittstellen und zu wenig Vertrauen in Klassen. Schnittstellen sind im Wesentlichen stark verwässerte Basisklassen, und es gibt Fälle, in denen die Verwendung von etwas Leichtem zu übermäßigem Code für Boilerplates führen kann.
Es ist überhaupt nichts Falsches daran, eine Methode zu überschreiben, sie aufzurufen
super.method()
und den Rest ihres Körpers nur Dinge tun zu lassen, die die Basisklasse nicht stören. Dies verstößt nicht gegen das Liskov-Substitutionsprinzip .Als Faustregel gilt, dass es eine schlechte Idee ist, so starr und dogmatisch in der Softwareentwicklung zu sein, auch wenn etwas im Allgemeinen eine bewährte Methode ist.
Also würde ich das Gesagte mit einem Körnchen Salz nehmen.
quelle
People put way too much faith into interfaces, and too little faith into classes.
Komisch. Ich neige dazu zu denken, dass das Gegenteil der Fall ist.Ich möchte diesbezüglich einen lingualen und semantischen Ansatz verfolgen. Eine Schnittstelle ist nicht da für
DRY
. Obwohl es neben einer abstrakten Klasse, die es implementiert, als Mechanismus für die Zentralisierung verwendet werden kann, ist es nicht für DRY gedacht.Eine Schnittstelle ist ein Vertrag.
IAnimal
Schnittstelle ist ein Alles-oder-Nichts-Vertrag zwischen Alligator, Katze, Hund und der Vorstellung eines Tieres. Im Wesentlichen heißt es: "Wenn Sie ein Tier sein wollen, müssen Sie entweder alle diese Eigenschaften und Merkmale haben oder Sie sind kein Tier mehr."Die Vererbung auf der anderen Seite ist ein Mechanismus zur Wiederverwendung. In diesem Fall kann Ihnen eine gute semantische Argumentation bei der Entscheidung helfen, welche Methode wohin gehen soll. Während zum Beispiel alle Tiere schlafen und gleich schlafen, werde ich keine Basisklasse dafür verwenden. Ich habe die
Sleep()
Methode zuerstIAnimal
als Betonung (semantisch) für das, was es braucht, um ein Tier zu sein, in die Benutzeroberfläche eingefügt. Dann verwende ich eine abstrakte Basisklasse für Katze, Hund und Alligator als Programmiertechnik, um mich nicht zu wiederholen.quelle
Ich bin mir nicht sicher, ob dies die beste Faustregel ist. Sie können eine
abstract
Methode in die Basisklasse einfügen und von jeder Klasse implementieren lassen. Da jedermammal
essen muss, wäre es sinnvoll, das zu tun. Dannmammal
kann jeder seine eineeat
Methode anwenden. Normalerweise verwende ich nur Schnittstellen für die Kommunikation zwischen Objekten oder als Marker, um eine Klasse als etwas zu markieren. Ich denke, dass die Überbeanspruchung von Schnittstellen zu schlampigem Code führt.Ich stimme auch @Panzercrisis zu, dass eine Faustregel nur eine allgemeine Richtlinie sein sollte. Nicht etwas, das in Stein gemeißelt sein sollte. Es wird zweifellos Zeiten geben, in denen es einfach nicht funktioniert.
quelle
Schnittstellen sind Typen. Sie bieten keine Implementierung. Sie definieren einfach den Vertrag für die Bearbeitung dieses Typs. Dieser Vertrag muss von jeder Klasse erfüllt werden, die die Schnittstelle implementiert.
Die Verwendung von Schnittstellen und abstrakten / Basisklassen sollte durch die Entwurfsanforderungen bestimmt werden.
Eine einzelne Klasse benötigt keine Schnittstelle.
Wenn zwei Klassen innerhalb einer Funktion austauschbar sind, sollten sie eine gemeinsame Schnittstelle implementieren. Jede Funktionalität, die identisch implementiert ist, könnte dann in eine abstrakte Klasse umgestaltet werden, die die Schnittstelle implementiert. Die Schnittstelle könnte zu diesem Zeitpunkt als redundant angesehen werden, wenn keine Unterscheidungsfunktion vorhanden ist. Aber was ist dann der Unterschied zwischen den beiden Klassen?
Die Verwendung abstrakter Klassen als Typen ist im Allgemeinen unübersichtlich, wenn mehr Funktionen hinzugefügt und eine Hierarchie erweitert wird.
Bevorzugen Sie die Komposition gegenüber der Vererbung - all dies verschwindet.
quelle