Wann sollte ich beim Entwerfen von C # -Klassenbibliotheken die Vererbung einer Schnittstelle vorziehen? [geschlossen]

77

Ich habe eine ProcessorZahlenklasse, die zwei sehr unterschiedliche Dinge tut, aber vom gemeinsamen Code aufgerufen wird (eine "Umkehrung der Kontrolle" -Situation).

Ich frage mich, welche Designüberlegungen ich berücksichtigen sollte (oder für Sie US-Benutzer berücksichtigen sollte), wenn ich entscheide, ob sie alle von einer Schnittstelle erben BaseProcessoroder IProcessorals Schnittstelle implementieren sollen .

wahrscheinlich am Strand
quelle
4
Siehe auch die allgemeinere Frage, die nicht spezifisch für C # ist: Schnittstelle gegen Basisklasse
Cody Gray

Antworten:

185

Im Allgemeinen lautet die Regel ungefähr so:

  • Vererbung beschreibt eine Beziehung.
  • Das Implementieren einer Schnittstelle beschreibt eine Can-Do- Beziehung.

Um dies etwas konkreter auszudrücken, schauen wir uns ein Beispiel an. Die System.Drawing.BitmapKlasse ist-ein Bild (und als solche, es erbt von der ImageKlasse), aber es auch Can-do entsorgen, so dass es die implementiert IDisposableSchnittstelle. Es kann auch Serialisierung durchführen, sodass es über die ISerializableSchnittstelle implementiert wird.

In der Praxis werden Schnittstellen jedoch häufig verwendet, um die Mehrfachvererbung in C # zu simulieren. Wenn Ihre ProcessorKlasse von so etwas erben muss System.ComponentModel.Component, haben Sie keine andere Wahl, als eine IProcessorSchnittstelle zu implementieren .

Tatsache ist, dass sowohl Schnittstellen als auch abstrakte Basisklassen einen Vertrag bereitstellen, der angibt, was eine bestimmte Klasse tun kann. Es ist ein weit verbreiteter Mythos, dass Schnittstellen erforderlich sind, um diesen Vertrag zu deklarieren, aber das ist nicht korrekt. Der größte Vorteil für mich ist, dass Sie mit abstrakten Basisklassen Standardfunktionen für die Unterklassen bereitstellen können. Wenn es jedoch keine sinnvolle Standardfunktionalität gibt, hindert Sie nichts daran, die Methode selbst als zu abstractkennzeichnen, sodass abgeleitete Klassen sie selbst implementieren müssen, genau wie wenn sie eine Schnittstelle implementieren würden.

Für Antworten auf Fragen wie diese wende ich mich häufig den .NET Framework-Entwurfsrichtlinien zu , die Folgendes zur Auswahl zwischen Klassen und Schnittstellen enthalten:

Im Allgemeinen sind Klassen das bevorzugte Konstrukt zum Offenlegen von Abstraktionen.

Der Hauptnachteil von Schnittstellen besteht darin, dass sie viel weniger flexibel als Klassen sind, wenn es darum geht, die Entwicklung von APIs zu ermöglichen. Sobald Sie eine Schnittstelle versenden, ist die Anzahl ihrer Mitglieder für immer festgelegt. Alle Ergänzungen der Schnittstelle würden vorhandene Typen, die die Schnittstelle implementieren, beschädigen.

Eine Klasse bietet viel mehr Flexibilität. Sie können Mitglieder zu Klassen hinzufügen, die Sie bereits ausgeliefert haben. Solange die Methode nicht abstrakt ist (dh solange Sie eine Standardimplementierung der Methode bereitstellen), funktionieren vorhandene abgeleitete Klassen weiterhin unverändert.

[. . . ]]

Eines der häufigsten Argumente für Schnittstellen ist, dass sie die Trennung von Vertrag und Implementierung ermöglichen. Das Argument geht jedoch fälschlicherweise davon aus, dass Sie Verträge nicht mithilfe von Klassen von der Implementierung trennen können. Abstrakte Klassen, die sich in einer von ihren konkreten Implementierungen getrennten Baugruppe befinden, sind eine gute Möglichkeit, eine solche Trennung zu erreichen.

Ihre allgemeinen Empfehlungen lauten wie folgt:

  • Sie favorisieren Klassen über Schnittstellen zu definieren.
  • Sie verwenden abstrakte Klassen statt Schnittstellen den Vertrag von Implementierungen zu entkoppeln. Abstrakte Klassen ermöglichen bei korrekter Definition den gleichen Grad an Entkopplung zwischen Vertrag und Implementierung.
  • Sie eine Schnittstelle definieren , wenn Sie eine polymorphe Hierarchie von Werttypen zur Verfügung stellen müssen.
  • Ziehen Sie in Betracht , Schnittstellen zu definieren, um einen ähnlichen Effekt wie bei der Mehrfachvererbung zu erzielen.

Chris Anderson stimmt diesem letzten Grundsatz besonders zu und argumentiert:

Abstrakte Typen machen die Version viel besser und ermöglichen zukünftige Erweiterbarkeit, aber sie brennen auch Ihren einzigen Basistyp. Schnittstellen sind geeignet, wenn Sie tatsächlich einen Vertrag zwischen zwei Objekten definieren, der über die Zeit unveränderlich ist. Abstrakte Basistypen eignen sich besser zum Definieren einer gemeinsamen Basis für eine Typenfamilie.

Cody Grey
quelle
1
danke - tolle Antwort ... Ditto. mit nur einem Nachteile: wenn du gehst zu veröffentlichen eine Basis-Klasse als „public Typ“ dann die Benutzer dieser Klasse zu vererben es mit dort eigenen Basisklasse beraten ... sie irgendwo zu geben , es gemeinsame zu setzen Verhalten (in Zukunft) .... Und vielen Dank für den Link. Ich habe eine gute Lektüre zu tun ;-)
Corlettk
17
Es sollte beachtet werden, dass eine Schnittstelle eher ein "Muss" ist.
Awiebe
Diese Antwort wird angesichts der jüngsten Änderungen in Java 8 zur Einführung von Standardmethoden in Schnittstellen wahrer. Andernfalls gab es keine andere Möglichkeit, einen neuen Vertrag / eine neue Methode einzuführen, ohne jeden Code zu beschädigen, der eine Schnittstelle implementiert. (Obwohl die Grenze zwischen abstrakter Klasse und Schnittstelle mit dieser Änderung verschwommener wird)
Harshdeep
1
Ist dieser Rat nicht fast das Gegenteil von "Head First Design Patterns"? Ich bin keineswegs ein Experte, aber ihr Mantra war lieber Komposition als Vererbung. Kann jemand das entwirren?
Alimack
@ali Ich sehe den Widerspruch überhaupt nicht. Zugegeben, es ist viele Jahre her, seit ich diese Antwort geschrieben habe, aber wenn ich sie noch einmal überfliege, sehe ich nirgendwo, wo ich mich für die Vererbung gegenüber der Komposition einsetze. Es sagt eigentlich nichts über Komposition aus. Wenn Sie die durch Vererbung modellierte starke Beziehung nicht benötigen, ist die Komposition die bessere Wahl. Drücken Sie keine stärkere Beziehung aus, als Sie brauchen.
Cody Gray
10

Richard,

Warum zwischen ihnen wählen? Ich hätte eine IProzessor-Schnittstelle als veröffentlichten Typ (zur Verwendung an anderer Stelle im System). und wenn es so kommt, dass Ihre verschiedenen CURRENT-Implementierungen von IProcessor ein gemeinsames Verhalten aufweisen, ist eine abstrakte BaseProcessor-Klasse ein wirklich guter Ort, um dieses gemeinsame Verhalten zu implementieren.

Auf diese Weise muss ein IProzessor, der in Zukunft NICHT für die Dienste von BaseProcessor vorgesehen ist, nicht vorhanden sein (und möglicherweise ausgeblendet werden) in dupliziertem Code / Konzepten.

Nur meine bescheidene MEINUNG.

Prost. Keith.

Corlettk
quelle
Danke Keith - das ist ein guter Punkt. Ich war wirklich auf der Suche, ob es irgendwelche Überlegungen gibt, die mich bei der Auswahl einer über die andere beißen könnten. Die Implementierung von beiden deckt das gut ab.
wahrscheinlich am Strand
1
Hmm, ich bin damit nicht einverstanden. Sie sollten Schnittstellen im Allgemeinen nicht ohne konkrete Implementierungen dieser Schnittstelle verfügbar machen. Ein weiteres Problem beim Offenlegen der Benutzeroberfläche ist die Versionierung (siehe meine Antwort für Details). Ich glaube nicht, dass Sie beides tun müssen. Das ist nicht immer die beste Antwort auf Entwurfsentscheidungen. Der Versuch, beides zu tun, ist mit einer erhöhten Komplexität verbunden, und ich habe Mühe zu sehen, wie vorteilhaft das hier wirklich ist. Es gibt nichts, was Sie verpassen, wenn Sie einfach eine abstrakte Basisklasse anstelle einer Schnittstelle verwenden.
Cody Gray
5

Schnittstellen sind ein "Vertrag", der sicherstellt, dass einige Klassen eine gewünschte Gruppe von Mitgliedern implementieren - Eigenschaften, Methoden und Ereignisse -.

Basisklassen (konkret oder abstrakt, spielt keine Rolle) sind der Archetyp einer Entität. Das sind Entitäten, die darstellen, was in einer tatsächlichen physischen oder konzeptuellen Einheit üblich ist.

Wann sind Schnittstellen zu verwenden?

Wann immer ein Typ deklarieren muss, dass er zumindest einige Verhaltensweisen und Eigenschaften aufweist, die ein Verbraucher beachten und verwenden sollte, um eine Aufgabe zu erfüllen.

Wann werden Basisklassen verwendet (konkret und / oder abstrakt)?

Immer wenn eine Gruppe von Entitäten denselben Archetyp hat, bedeutet dies, dass B A erbt, weil B A mit Unterschieden ist, B jedoch als A identifiziert werden kann.


Beispiele:

Reden wir über Tische.

  • Wir akzeptieren recycelbare Tabellen => Dies muss mit einer Schnittstelle wie "IRecyclableTable" definiert werden, um sicherzustellen, dass alle recycelbaren Tabellen eine "Recycle" -Methode haben .

  • Wir wollen Desktop-Tabellen => Dies muss mit Vererbung definiert werden. Eine "Desktop-Tabelle" ist eine "Tabelle". Alle Tabellen haben gemeinsame Eigenschaften und Verhaltensweisen, und Desktop-Tabellen haben dieselben Eigenschaften, die solche Dinge hinzufügen, die dazu führen, dass eine Desktop-Tabelle anders funktioniert als andere Tabellentypen.

Ich könnte über Assoziationen sprechen, die Bedeutung beider Fälle in einem Objektgraphen, aber meiner bescheidenen Meinung nach würde ich genau mit dieser Argumentation antworten, wenn ich Argumente aus konzeptioneller Sicht liefern muss.

Matías Fidemraizer
quelle
2
Ich bin mir nicht sicher, was eine "recycelbare Tabelle" ist, aber ich würde wahrscheinlich eine IRecyclableSchnittstelle implementieren , da es wahrscheinlich ist, dass mehrere nicht verwandte Objekte recycelbar sein müssen (dh andere Klassen, die keine Tabellen darstellen). Und dann hätte ich immer noch alle Tabellen aus der TableKlasse implementiert , einschließlich DesktopTableund PicnicTableund FoldingTable.
Cody Gray
Und wenn Sie meinen Text noch einmal überprüfen, finden Sie dann etwas anderes als Sie? :) Lesen Sie, wie die Sache mit "recycelbaren Tabellen" beginnt: "Beispiele". Es ist nur ein Beispiel, absolut isoliert von jedem Objektgraphen, Design oder tatsächlichen Fall.
Matías Fidemraizer
Wie auch immer, danke für jeden Vorschlag, irre dich nicht mit meinen Worten, hey! :)
Matías Fidemraizer
Einige sagen "Schnittstellen sind kein Vertrag" : blogs.msdn.microsoft.com/kcwalina/2004/10/24/…
MikeSchinkel
2

Ich bin nicht sehr gut in der Auswahl von Designs, aber wenn ich gefragt werde, bevorzuge ich die Implementierung einer iProcessor-Oberfläche, wenn nur Mitglieder erweitert werden müssen. Wenn es andere Funktionen gibt, die nicht erweitert werden müssen, ist das Erben vom Basisprozessor die bessere Option.

jitendragarg
quelle
@jitendra - danke, könnten Sie näher erläutern, warum dies eine bessere Option ist?
wahrscheinlich am Strand
Warum bevorzugen Sie Schnittstellen? Was ist der Vorteil einer Schnittstelle gegenüber einer abstrakten Basisklasse? Ich stimme Ihren Instinkten überhaupt nicht zu - ich würde immer eine abstrakte Basisklasse über eine Schnittstelle wählen, es sei denn, ich musste speziell die Mehrfachvererbung simulieren.
Cody Gray
Wenn nur wenige Funktionen vorhanden sind, ist das Erben sinnvoller, da Sie nicht alle in der Basisklasse verfügbaren Funktionen implementieren müssen. Und es ist nicht erforderlich, dass Sie über genügend Informationen zur vollständigen Liste der Funktionen und Parameter der Basisklasse verfügen. In diesem Fall ist das Erben also sinnvoller.
Jitendragarg
Außerdem können Sie anstelle einer Zusammenfassung immer eine normale Basisklasse erstellen und nur die erforderlichen Funktionen erben, auch wenn die Basisklasse später verwendet werden soll. In diesem Szenario wird es jedoch zu Leistungseinbußen kommen, da die Schnittstellen angeblich leichter als Klassen sind. Wenn Sie die Schnittstelle über eine abstrakte Klasse verwenden, müssen andere Entwickler nicht einmal jede Funktion implementieren, um nur einige Funktionen aus der Basisklasse zu verwenden. Daher ist die Wiederverwendbarkeit der Basisklasse ebenfalls ein Faktor, den Sie berücksichtigen sollten, bevor Sie solche Entscheidungen treffen.
Jitendragarg
@cody grey Wenn ich mich nicht irre, solltest du die gesamte abstrakte Klasse implementieren, was im Falle einer Wiederverwendbarkeit eine schlechte Idee ist. Auch das ist nur mein Standpunkt. Könnte falsch sein. Bitte bearbeiten Sie den Kommentar, falls falsch.
Jitendragarg
2

Abgesehen von der Designreinheit können Sie nur einmal erben. Wenn Sie also eine Framework-Klasse erben müssen, ist die Frage umstritten.

Wenn Sie eine Wahl haben, können Sie pragmatisch die Option wählen, die die meisten Eingaben spart. Es ist normalerweise sowieso die puristische Wahl.

BEARBEITEN:

Wenn die Basisklasse eine Implementierung hat, kann dies nützlich sein. Wenn sie rein abstact ist, kann sie auch eine Schnittstelle sein.

Jodrell
quelle
Das ist ein guter Punkt. Oft spart die Auswahl einer Basisklasse die meiste Eingabe. Wenn Sie eine Methode mit mehreren Überladungen haben, die alle eine Kernfunktion aufrufen, können Sie alle Parameterüberprüfungen, Grenzwertprüfungen und Funktionsüberladungen in die Basisklasse einfügen. Das bedeutet, dass die abgeleiteten Klassen nur die Kernfunktionalität "Fleisch und Kartoffeln" implementieren müssen. Dies ist keine Option mit einer Schnittstelle, bei der jede Klasse, die diese Schnittstelle implementiert, jedes Mal die ganze Arbeit erledigen müsste.
Cody Gray
@Cody Gray Über den letzten Teil, aber vielleicht implementiert eine Basisklasse eine Schnittstelle, und die Implementierung der Schnittstelle ist virtuell, also ist dies ein gemischter Fall! Nur ein Scherz, egal.
Matías Fidemraizer
1

Angesichts der Tatsache, dass die SOLID-Prinzipien Ihrem Projekt mehr Wartbarkeit und Erweiterbarkeit bieten, würde ich Schnittstellen der Vererbung vorziehen.

Wenn Sie Ihrer Schnittstelle "zusätzliche Funktionen" hinzufügen müssen, ist es am besten, eine neue Schnittstelle zu erstellen, die dem I in SOLID folgt, dem Prinzip der Schnittstellensegregation.

Fernando Martínez
quelle
0

Wenn Sie keine Rolle spielen, wählen Sie immer Schnittstelle. Es ermöglicht mehr Flexibilität. Es könnte Sie vor zukünftigen Änderungen schützen (wenn Sie etwas in der Basisklasse ändern müssen, sind möglicherweise die geerbten Klassen betroffen). Es ermöglicht auch, die Details besser zu kapseln. Wenn Sie eine Inversion der Steuerung der Abhängigkeitsinjektion verwenden, bevorzugen diese tendenziell die Schnittstelle.

Oder wenn eine Vererbung nicht vermieden werden kann, ist es wahrscheinlich eine gute Idee, beide zusammen zu verwenden. Erstellen Sie eine abstrakte Basisklasse, die eine Schnittstelle implementiert. In Ihrem Fall implementiert ProcessorBase einen IProzessor.

Ähnlich wie ControllerBase und IController in ASP.NET Mvc.

Hendry Ten
quelle
3
Es ist interessant, dass Sie argumentieren, dass Schnittstellen flexibler sind, was dies mit dem Fall zukünftiger Änderungen rechtfertigt. Was ist, wenn Sie in Zukunft Ihrer Benutzeroberfläche einige Funktionen hinzufügen müssen? Das wäre eine bahnbrechende Änderung, da jede Klasse, die diese Schnittstelle implementiert hat, auch die gerade hinzugefügte Methode implementieren müsste . Dies ist bei einer Basisklasse nicht der Fall, da die Basisklasse einfach eine Standardimplementierung bereitstellen könnte.
Cody Gray
Jeder Fall von "zukünftigen Veränderungen" hat unterschiedliche Geschichten. In Ihrem Fall ist es dann besser, eine abstrakte Klasse zu verwenden. Dabei bevorzuge ich möglicherweise auch abstrakte Klassen mit Schnittstellen (sowohl die I- als auch die -Base). Auch aus verschiedenen Blickwinkeln ist es vorzuziehen, zwei oder drei Methoden zu sehen, anstatt eine ganze Reihe von Methoden, wenn Sie die Klasse verwenden, die sie nicht entwirft. In diesem Fall ist die Schnittstelle bei der Trennung von Bedenken besser. Sie können eine Schnittstelle verwenden, um etwas zu tun, und eine andere, um etwas anderes zu tun. Und all diese Schnittstellen könnten tatsächlich eine einzige Klasse sein.
Hendry Ten
Informationen zum Hinzufügen von Funktionen zu einer Schnittstelle: Sie fügen der Schnittstelle im Allgemeinen keine neuen Funktionen hinzu (dies gilt nicht für den Vertragstyp oder das Repository). Sie können eine neue erstellen (die Schnittstelle in c # kann von einer anderen Schnittstelle abgeleitet werden), sofern die Bedenken getrennt sind ist gut umgesetzt.
Hendry Ten
1
Ich mag solche Argumente nicht. Design sollte aus Refactoring-Gründen nicht durchgeführt werden. Ein gutes Design lässt sich leicht umgestalten und es geht nicht um die Vererbung der Schnittstellennutzung. Softwareänderungen, klar, aber dies kann getan werden, indem man darauf achtet, welche Art von Anforderungen Überstunden lösen. Wenn ich mich also nicht irre, selbst bei Vererbung, würde ein guter Vererbungsbaum Logik hinzufügen, ändern und / oder entfernen, aber seine Bedeutung sollte "wie es ist" bleiben.
Matías Fidemraizer
Was @CodyGray und Matias gesagt haben, mal 10.
MikeSchinkel