Mein Kollege und ich haben unterschiedliche Meinungen zum Verhältnis zwischen Basisklassen und Interfaces. Ich bin der Meinung, dass eine Klasse keine Schnittstelle implementieren sollte, es sei denn, diese Klasse kann verwendet werden, wenn eine Implementierung der Schnittstelle erforderlich ist. Mit anderen Worten, ich sehe Code gerne so:
interface IFooWorker { void Work(); }
abstract class BaseWorker {
... base class behaviors ...
public abstract void Work() { }
protected string CleanData(string data) { ... }
}
class DbWorker : BaseWorker, IFooWorker {
public void Work() {
Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
}
}
Der DbWorker ist das, was die IFooWorker-Schnittstelle bekommt, weil es eine instantiierbare Implementierung der Schnittstelle ist. Es erfüllt den Vertrag vollständig. Mein Kollege bevorzugt das fast Identische:
interface IFooWorker { void Work(); }
abstract class BaseWorker : IFooWorker {
... base class behaviors ...
public abstract void Work() { }
protected string CleanData(string data) { ... }
}
class DbWorker : BaseWorker {
public void Work() {
Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
}
}
Wo die Basisklasse die Schnittstelle erhält, und auf diese Weise gehören auch alle Erben der Basisklasse zu dieser Schnittstelle. Das nervt mich, aber ich kann keine konkreten Gründe nennen, warum die Basisklasse als Implementierung der Schnittstelle außerhalb von "nicht für sich allein stehen kann".
Was sind die Vor- und Nachteile seiner Methode im Vergleich zu meiner und warum sollte eine Methode einer anderen vorgezogen werden?
quelle
Work
ist das Problem der Diamantvererbung (in diesem Fall erfüllen beide einen Vertrag über dieWork
Methode). In Java müssen Sie die Methoden der Schnittstelle implementieren, damit die Frage, welcheWork
Methode das Programm verwenden soll, auf diese Weise vermieden wird. Sprachen wie C ++ disambiguieren dies jedoch nicht für Sie.Antworten:
BaseWorker
erfüllt diese Anforderung. Nur weil Sie einBaseWorker
Objekt nicht direkt instanziieren können, heißt das nicht, dass Sie keinenBaseWorker
Zeiger haben können , der den Vertrag erfüllt. In der Tat ist das der springende Punkt bei abstrakten Klassen.Es ist auch schwierig, anhand des von Ihnen veröffentlichten vereinfachten Beispiels zu sagen, aber ein Teil des Problems kann sein, dass die Schnittstelle und die abstrakte Klasse redundant sind. Sofern Sie keine anderen Klassen implementieren
IFooWorker
, die nicht von diesen abgeleitet sindBaseWorker
, benötigen Sie die Schnittstelle überhaupt nicht. Schnittstellen sind nur abstrakte Klassen mit einigen zusätzlichen Einschränkungen, die die Mehrfachvererbung erleichtern.Auch in einem vereinfachten Beispiel ist die Verwendung einer geschützten Methode, die sich explizit auf die Basis einer abgeleiteten Klasse bezieht, und das Fehlen einer eindeutigen Stelle zum Deklarieren der Schnittstellenimplementierung ein Warnsignal dafür, dass Sie die Vererbung nicht verwenden Zusammensetzung. Ohne dieses Erbe wird Ihre ganze Frage streitig.
quelle
BaseWorker
eine gegebeneBaseWorker myWorker
nicht implementiert wird, nur weil sie nicht direkt instanziierbar istIFooWorker
.Ich muss Ihrem Kollegen zustimmen.
In beiden Beispielen definiert BaseWorker die abstrakte Methode Work (), was bedeutet, dass alle Unterklassen den IFooWorker-Vertrag erfüllen können. In diesem Fall sollte BaseWorker die Schnittstelle implementieren, und diese Implementierung wird von den Unterklassen übernommen. Dies erspart Ihnen die explizite Angabe, dass jede Unterklasse tatsächlich ein IFooWorker (das DRY-Prinzip) ist.
Wenn Sie Work () nicht als Methode von BaseWorker definieren oder wenn IFooWorker andere Methoden hat, die Unterklassen von BaseWorker nicht benötigen oder wollen, müssen Sie (offensichtlich) angeben, welche Unterklassen IFooWorker tatsächlich implementieren.
quelle
IFoo
und einBaseFoo
haben, bedeutet dies, dass es sich entwederIFoo
nicht um eine entsprechend fein abgestimmte Schnittstelle handelt oder dassBaseFoo
eine Vererbung verwendet wird, bei der die Komposition möglicherweise die bessere Wahl ist.MyDaoFoo
oderSqlFoo
oderHibernateFoo
was auch immer, um anzuzeigen, dass es sich nur um einen möglichen Baum von Klassen handelt, die implementiert werdenIFoo
? Es ist für mich noch riechender, wenn eine Basisklasse an eine bestimmte Bibliothek oder Plattform gekoppelt ist und dies im Namen nicht erwähnt wird ...Ich stimme in der Regel Ihrem Kollegen zu.
Nehmen wir Ihr Modell: Die Schnittstelle wird nur von den untergeordneten Klassen implementiert, obwohl die Basisklasse auch die Offenlegung von IFooWorker-Methoden erzwingt. Zunächst einmal ist es überflüssig; Unabhängig davon, ob die untergeordnete Klasse die Schnittstelle implementiert oder nicht, müssen sie die bereitgestellten Methoden von BaseWorker überschreiben. Ebenso muss jede IFooWorker-Implementierung die Funktionalität bereitstellen, unabhängig davon, ob sie Hilfe von BaseWorker erhalten oder nicht.
Dies macht Ihre Hierarchie zusätzlich mehrdeutig. "Alle IFooWorker sind BaseWorker" ist nicht unbedingt eine wahre Aussage, und "Alle BaseWorker sind IFooWorker" auch nicht. Wenn Sie also eine Instanzvariable, einen Parameter oder einen Rückgabetyp definieren möchten, bei dem es sich um eine Implementierung von IFooWorker oder BaseWorker handeln kann (wobei die allgemein verfügbare Funktionalität genutzt wird, die einer der Gründe ist, die an erster Stelle geerbt werden sollten), ist dies nicht der Fall diese sind garantiert allumfassend; Einige BaseWorker können keiner Variablen vom Typ IFooWorker zugewiesen werden und umgekehrt.
Das Modell Ihres Kollegen ist viel einfacher zu bedienen und zu ersetzen. "Alle BaseWorker sind IFooWorker" ist jetzt eine wahre Aussage; Sie können jeder IFooWorker-Abhängigkeit jede BaseWorker-Instanz zuweisen, ohne Probleme. Die gegenteilige Aussage "Alle IFooWorker sind BaseWorker" ist nicht wahr; Dadurch können Sie BaseWorker durch BetterBaseWorker ersetzen, und Ihr konsumierender Code (der von IFooWorker abhängt) muss den Unterschied nicht feststellen.
quelle
Ich muss diesen Antworten eine Warnung hinzufügen. Das Koppeln der Basisklasse an die Schnittstelle erzeugt eine Kraft in der Struktur dieser Klasse. In Ihrem grundlegenden Beispiel ist es ein Kinderspiel, dass die beiden gekoppelt werden sollten, aber das kann nicht in allen Fällen zutreffen.
Nehmen Sie an Javas Klassen für das Collection Framework teil:
Die Tatsache, dass der Queue- Vertrag von LinkedList implementiert wird , hat das Anliegen nicht in AbstractList verdrängt .
Was ist der Unterschied zwischen den beiden Fällen? Der Zweck von BaseWorker war immer (wie durch seinen Namen und seine Schnittstelle mitgeteilt), Operationen in IWorker zu implementieren . Der Zweck von AbstractList und der von Queue sind unterschiedlich, aber ein Nachkomme des ersteren kann den letztgenannten Vertrag noch umsetzen.
quelle
Ich würde die Frage stellen, was passiert, wenn Sie
IFooWorker
eine neue Methode hinzufügen?Wenn
BaseWorker
die Schnittstelle implementiert ist, muss sie standardmäßig die neue Methode deklarieren, auch wenn sie abstrakt ist. Wenn die Schnittstelle dagegen nicht implementiert wird, treten nur Kompilierungsfehler für die abgeleiteten Klassen auf.Aus diesem Grund würde ich die Basisklasse veranlassen, die Schnittstelle zu implementieren , da ich möglicherweise alle Funktionen für die neue Methode in der Basisklasse implementieren kann , ohne die abgeleiteten Klassen zu berühren.
quelle
Stellen Sie sich zunächst vor, was eine abstrakte Basisklasse und was eine Schnittstelle ist. Überlegen Sie, wann Sie das eine oder andere verwenden würden und wann nicht.
Es ist üblich, dass die Leute denken, dass beide Konzepte sehr ähnlich sind. Tatsächlich handelt es sich um eine häufig gestellte Interviewfrage (der Unterschied zwischen den beiden ist ??).
Eine abstrakte Basisklasse gibt Ihnen also etwas, was Interfaces nicht können, nämlich Standardimplementierungen von Methoden. (Es gibt noch andere Dinge, in C # können Sie statische Methoden für eine abstrakte Klasse verwenden, beispielsweise nicht für eine Schnittstelle.)
Dies wird beispielsweise häufig mit IDisposable verwendet. Die abstrakte Klasse implementiert die Dispose-Methode von IDisposable. Dies bedeutet, dass alle abgeleiteten Versionen der Basisklasse automatisch verfügbar sind. Sie können dann mit mehreren Optionen spielen. Machen Sie Dispose abstrakt und zwingen Sie abgeleitete Klassen, es zu implementieren. Stellen Sie eine virtuelle Standardimplementierung bereit, damit diese nicht ausgeführt wird, oder machen Sie sie weder virtuell noch abstrakt, und lassen Sie sie virtuelle Methoden aufrufen, die beispielsweise BeforeDispose, AfterDispose, OnDispose oder Disposing heißen.
Jedes Mal, wenn alle abgeleiteten Klassen das Interface unterstützen müssen, wird es auf die Basisklasse angewendet. Wenn nur eine oder einige diese Schnittstelle benötigen, wird sie für die abgeleitete Klasse verwendet.
Das ist eigentlich alles eine grobe Vereinfachung. Eine andere Alternative besteht darin, dass abgeleitete Klassen die Schnittstellen nicht einmal implementieren, sondern über ein Adaptermuster bereitstellen. Ein Beispiel dafür, das ich kürzlich gesehen habe, war in IObjectContextAdapter.
quelle
Ich bin noch Jahre davon entfernt, die Unterscheidung zwischen einer abstrakten Klasse und einer Schnittstelle zu verstehen. Jedes Mal, wenn ich denke, die Grundkonzepte in den Griff zu bekommen, schaue ich auf StackExchange und bin zwei Schritte zurück. Aber ein paar Gedanken zum Thema und zur OP-Frage:
Zuerst:
Es gibt zwei allgemeine Erklärungen für eine Schnittstelle:
Eine Schnittstelle ist eine Liste von Methoden und Eigenschaften, die jede Klasse implementieren kann. Durch die Implementierung einer Schnittstelle garantiert eine Klasse, dass diese Methoden (und ihre Signaturen) und diese Eigenschaften (und ihre Typen) verfügbar sind, wenn eine "Schnittstelle" mit dieser Klasse oder erstellt wird ein Objekt dieser Klasse. Eine Schnittstelle ist ein Vertrag.
Schnittstellen sind abstrakte Klassen, die nichts tun / können. Sie sind nützlich, weil Sie im Gegensatz zu diesen mittleren Elternklassen mehr als eine implementieren können. Ich könnte ein Objekt der Klasse BananaBread sein und von BaseBread erben, aber das heißt nicht, dass ich nicht auch die Schnittstellen IWithNuts und ITastesYummy implementieren kann. Ich könnte sogar die IDoesTheDishes-Schnittstelle implementieren, weil ich nicht nur Brot bin, weißt du?
Es gibt zwei allgemeine Erklärungen für eine abstrakte Klasse:
Eine abstrakte Klasse ist das , was das Ding nicht kann. Es ist wie die Essenz, überhaupt keine echte Sache. Warte, das wird helfen. Ein Boot ist eine abstrakte Klasse, aber eine sexy Playboy-Yacht wäre eine Unterklasse von BaseBoat.
Ich habe ein Buch über abstrakte Klassen gelesen, und vielleicht sollten Sie dieses Buch lesen, weil Sie es wahrscheinlich nicht verstehen und es falsch machen, wenn Sie dieses Buch nicht gelesen haben.
Übrigens, die Buchziter wirken immer beeindruckend, auch wenn ich immer noch verwirrt davongehe.
Zweite:
Auf SO fragte jemand eine einfachere Version dieser Frage, den Klassiker: "Warum Schnittstellen verwenden? Was ist der Unterschied? Was fehle ich?" In einer Antwort wurde ein Luftwaffenpilot als einfaches Beispiel verwendet. Es landete nicht ganz, aber es löste einige großartige Kommentare aus, von denen einer die IFlyable-Schnittstelle mit Methoden wie takeOff, pilotEject usw. erwähnte. aber entscheidend. Eine Schnittstelle macht ein Objekt / eine Klasse intuitiv oder gibt zumindest den Sinn, den es hat. Eine Schnittstelle dient nicht dem Nutzen des Objekts oder der Daten, sondern etwas, das mit diesem Objekt interagieren muss. Der Klassiker Obst-> Apfel-> Fuji oder Form-> Dreieck-> Gleichseitige Beispiele für die Vererbung sind ein hervorragendes Modell für das taxonomische Verständnis eines bestimmten Objekts anhand seiner Nachkommen. Es informiert den Verbraucher und den Verarbeiter über seine allgemeinen Eigenschaften, Verhaltensweisen, ob es sich bei dem Objekt um eine Gruppe von Dingen handelt, wird Ihr System beschädigen, wenn Sie es an der falschen Stelle ablegen, oder es beschreibt sensorische Daten, einen Konnektor zu einem bestimmten Datenspeicher oder Finanzdaten für die Gehaltsabrechnung.
Ein bestimmtes Flugzeugobjekt kann eine Methode für eine Notlandung haben oder nicht, aber ich bin sauer, wenn ich davon ausgehe, dass das Notland wie jedes andere flugfähige Objekt in DumbPlane aufgewacht ist, und erfahre, dass die Entwickler Quickland gewählt haben weil sie dachten, dass es egal sei. Genauso wie ich frustriert wäre, wenn jeder Schraubenhersteller eine eigene Interpretation von Righty Tighty hätte oder wenn mein Fernseher nicht den Lautstärkeregler über dem Lautstärkeregler hätte.
Abstrakte Klassen sind das Modell, das festlegt, was ein untergeordnetes Objekt als diese Klasse qualifizieren muss. Wenn Sie nicht alle Qualitäten einer Ente haben, spielt es keine Rolle, dass Sie das IQuack-Interface implementiert haben, Ihr einziger komischer Pinguin. Schnittstellen sind die Dinge, die Sinn machen, auch wenn Sie sich bei nichts anderem sicher sind. Jeff Goldblum und Starbuck waren beide in der Lage, außerirdische Raumschiffe zu fliegen, da die Benutzeroberfläche zuverlässig ähnlich war.
Dritte:
Ich stimme Ihrem Kollegen zu, denn manchmal müssen Sie bestimmte Methoden von Anfang an durchsetzen. Wenn Sie ein Active Record-ORM erstellen, ist eine Speichermethode erforderlich. Dies hängt nicht von der Unterklasse ab, die instanziiert werden kann. Und wenn die ICRUD-Schnittstelle portabel genug ist, um nicht ausschließlich mit einer abstrakten Klasse gekoppelt zu werden, kann sie von anderen Klassen implementiert werden, um sie für jeden, der bereits mit einer der abgeleiteten Klassen dieser abstrakten Klasse vertraut ist, zuverlässig und intuitiv zu machen.
Auf der anderen Seite gab es bereits ein gutes Beispiel dafür, wann nicht eine Schnittstelle an die abstrakte Klasse gebunden werden sollte, da nicht alle Listentypen eine Warteschlangenschnittstelle implementieren (oder sollten). Sie sagten, dass dieses Szenario die Hälfte der Zeit passiert, was bedeutet, dass Sie und Ihr Kollege beide die Hälfte der Zeit falsch liegen. Daher ist es das Beste, zu argumentieren, zu debattieren, zu überlegen und die Kopplung zu akzeptieren, wenn sie sich als richtig herausstellt . Werden Sie jedoch kein Entwickler, der einer Philosophie folgt, auch wenn diese für den jeweiligen Job nicht die beste ist.
quelle
Es gibt noch einen weiteren Aspekt, warum das
DbWorker
implementiert werden sollteIFooWorker
, wie Sie vorschlagen.Im Falle des Stils Ihres Mitarbeiters verliert die Schnittstelle , wenn aus irgendeinem Grund ein späteres Refactoring stattfindet und davon
DbWorker
ausgegangen wird, dass die abstrakteBaseWorker
Klasse nicht erweitert wird. Dies könnte, muss aber nicht, Auswirkungen auf die Kunden haben, die es verbrauchen, wenn sie die Schnittstelle erwarten .DbWorker
IFooWorker
IFooWorker
quelle
Zu Beginn definiere ich Interfaces und abstrakte Klassen gerne anders:
In Ihrem Fall setzen alle Mitarbeiter um,
work()
weil der Vertrag dies von ihnen verlangt. Daher sollte Ihre BasisklasseBaseworker
diese Methode entweder explizit oder als virtuelle Funktion implementieren. Ich würde vorschlagen, dass Sie diese Methode in Ihre Basisklasse aufnehmen, da alle Worker dies tun müssenwork()
. Daher ist es logisch, dass Ihre Basisklasse (obwohl Sie keinen Zeiger dieses Typs erstellen können) diese als Funktion enthält.DbWorker
Befriedigt jetzt dieIFooWorker
Schnittstelle, aber wenn es eine bestimmte Art gibt, die diese Klasse tutwork()
, muss sie die Definition, von der sie geerbt wurde, wirklich überladenBaseWorker
. Der Grund, warum es die Schnittstelle nichtIFooWorker
direkt implementieren sollte, ist, dass dies nichts Besonderes istDbWorker
. Wenn Sie dies jedes Mal tun, wenn Sie ähnliche Klassen implementierenDbWorker
, verletzen Sie DRY .Wenn zwei Klassen dieselbe Funktion auf unterschiedliche Weise implementieren, können Sie nach der größten gemeinsamen Superklasse suchen. Meistens werden Sie einen finden. Wenn nicht, schauen Sie weiter oder geben Sie auf und akzeptieren Sie, dass sie nicht genug Gemeinsamkeiten haben, um eine Basisklasse zu bilden.
quelle
Wow, all diese Antworten und keine, die darauf hinweisen, dass die Schnittstelle im Beispiel überhaupt keinen Zweck erfüllt. Sie verwenden keine Vererbung und keine Schnittstelle für dieselbe Methode, da dies sinnlos ist. Sie benutzen den einen oder anderen. In diesem Forum gibt es viele Fragen mit Antworten, in denen die Vorteile der einzelnen und der jeweils erforderlichen Szenarien erläutert werden.
quelle