it-swarm.com.de

Schnittstellen zu einer abstrakten Klasse

Mein Kollege und ich haben unterschiedliche Meinungen über die Beziehung zwischen Basisklassen und Schnittstellen. Ich bin der Überzeugung, 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 gerne Code wie diesen:

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 erhält die IFooWorker-Schnittstelle, da es sich um eine instanziierbare Implementierung der Schnittstelle handelt. 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 aufgrund dessen gehören auch alle Erben der Basisklasse zu dieser Schnittstelle. Das nervt mich, aber ich kann keine konkreten Gründe finden, warum außerhalb von "die Basisklasse nicht alleine als Implementierung der Schnittstelle stehen kann".

Was sind die Vor- und Nachteile seiner Methode gegenüber meiner und warum sollte eine über eine andere angewendet werden?

31
Bryan Boettcher

Ich bin der Überzeugung, dass eine Klasse keine Schnittstelle implementieren sollte, es sei denn, diese Klasse kann verwendet werden, wenn eine Implementierung der Schnittstelle erforderlich ist.

BaseWorker erfüllt diese Anforderung. Nur weil Sie ein BaseWorker -Objekt nicht direkt instanziieren können, heißt das nicht, dass Sie kein BaseWorkerZeiger haben können, das den Vertrag erfüllt. In der Tat ist das so ziemlich der springende Punkt bei abstrakten Klassen.

Es ist auch schwierig, anhand des von Ihnen veröffentlichten vereinfachten Beispiels zu erkennen, aber ein Teil des Problems kann darin bestehen, dass die Schnittstelle und die abstrakte Klasse redundant sind. Sofern Sie keine anderen Klassen haben, die IFooWorker implementieren, die nicht von BaseWorker ableiten, benötigen Sie die Schnittstelle überhaupt nicht. Schnittstellen sind nur abstrakte Klassen mit einigen zusätzlichen Einschränkungen, die die Mehrfachvererbung erleichtern.

Auch hier ist es schwierig, anhand eines vereinfachten Beispiels zu erkennen, dass die Verwendung einer geschützten Methode, die explizit auf die Basis einer abgeleiteten Klasse verweist, und das Fehlen eines eindeutigen Ortes zum Deklarieren der Schnittstellenimplementierung Warnsignale dafür sind, dass Sie die Vererbung unangemessen verwenden Komposition. Ohne dieses Erbe wird Ihre ganze Frage strittig.

24
Karl Bielefeldt

Ich muss Ihrem Kollegen zustimmen.

In beiden Beispielen definiert BaseWorker die abstrakte Methode Work (). Dies bedeutet, dass alle Unterklassen den Vertrag von IFooWorker erfüllen können. In diesem Fall sollte BaseWorker die Schnittstelle implementieren, und diese Implementierung würde von den Unterklassen übernommen. Dies erspart Ihnen die explizite Angabe, dass jede Unterklasse tatsächlich ein IFooWorker ist (das DRY -Prinzip).

Wenn Sie Work () nicht als eine Methode von BaseWorker definiert haben oder wenn IFooWorker andere Methoden hatte, die Unterklassen von BaseWorker nicht wollen oder brauchen würden, müssten Sie (offensichtlich) angeben, welche Unterklassen IFooWorker tatsächlich implementieren.

47
Matthew Flynn

Ich stimme im Allgemeinen 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 offengelegten Methoden von BaseWorker überschreiben. Ebenso muss jede IFooWorker-Implementierung die Funktionalität offenlegen, 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 beliebige Implementierung von IFooWorker oder BaseWorker handeln kann (unter Ausnutzung der allgemeinen exponierten Funktionalität, die einer der Gründe für die Vererbung ist), ist dies keines der beiden 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 verwenden und zu ersetzen. "Alle BaseWorker sind IFooWorker" ist jetzt eine wahre Aussage; Sie können jeder IFooWorker-Abhängigkeit jede BaseWorker-Instanz ohne Probleme zuweisen. Die entgegengesetzte Aussage "Alle IFooWorker sind BaseWorker" ist nicht wahr; Auf diese Weise können Sie BaseWorker durch BetterBaseWorker ersetzen, und Ihr konsumierender Code (der von IFooWorker abhängt) muss keinen Unterschied feststellen.

11
KeithS

Ich muss diesen Antworten eine Art Warnung hinzufügen. Durch das Koppeln der Basisklasse an die Schnittstelle wird eine Kraft in der Struktur dieser Klasse erzeugt. In Ihrem einfachen Beispiel ist es ein Kinderspiel, dass die beiden gekoppelt werden sollten, aber das gilt möglicherweise nicht in allen Fällen.

Nehmen Sie die Sammlungs-Framework-Klassen von Java:

abstract class AbstractList
class LinkedList extends AbstractList implements Queue

Die Tatsache, dass der Vertrag Queue von LinkedList implementiert wird, hat das Problem nicht in AbstractList verschoben.

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 letzteren Vertrag noch umsetzen.

6
Mihai Danila

Ich würde die Frage stellen, was passiert, wenn Sie IFooWorker ändern, z. B. indem Sie eine neue Methode hinzufügen?

Wenn BaseWorker die Schnittstelle implementiert, muss sie standardmäßig die neue Methode deklarieren, auch wenn sie abstrakt ist. Wenn die Schnittstelle jedoch nicht implementiert wird, werden nur Kompilierungsfehler für die abgeleiteten Klassen angezeigt.

Aus diesem Grund ich würde die Basisklasse die Schnittstelle implementieren lassen, da ich möglicherweise alle Funktionen implementieren kann für die neue Methode in der Basisklasse, ohne die abgeleiteten Klassen zu berühren.

2
Scott Whitlock

Ich bin noch Jahre davon entfernt, die Unterscheidung zwischen einer abstrakten Klasse und einer Schnittstelle vollständig zu erfassen. Jedes Mal, wenn ich denke, dass ich die grundlegenden Konzepte in den Griff bekomme, schaue ich mir Stackexchange an und bin zwei Schritte zurück. Aber ein paar Gedanken zum Thema und zur Frage der OP:

Zuerst:

Es gibt zwei allgemeine Erklärungen für eine Schnittstelle:

  1. 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 sie mit dieser Klasse oder dieser Klasse "verbunden" werden ein Objekt dieser Klasse. Eine Schnittstelle ist ein Vertrag.

  2. Schnittstellen sind abstrakte Klassen, die nichts tun können/können. Sie sind nützlich, weil Sie im Gegensatz zu diesen mittleren übergeordneten Klassen mehr als eine implementieren können. Ich könnte ein Objekt der Klasse BananaBread sein und von BaseBread erben, aber das bedeutet nicht, dass ich nicht sowohl die IWithNuts- als auch die ITastesYummy-Schnittstelle 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:

  1. Eine abstrakte Klasse ist, weißt du, das Ding nicht das, was das Ding 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.

  2. 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 werden, wenn Sie dieses Buch nicht gelesen haben.

Das Buch Citers wirkt übrigens immer beeindruckend, auch wenn ich immer noch verwirrt weggehe.

Zweite:

Auf SO stellte jemand eine einfachere Version dieser Frage, den Klassiker: "Warum Schnittstellen verwenden? Was ist der Unterschied? Was fehlt mir?" Und eine Antwort verwendete einen Luftwaffenpiloten als einfaches Beispiel. Es ist nicht ganz gelandet, aber es hat einige großartige Kommentare ausgelöst, von denen einer die IFlyable-Schnittstelle mit Methoden wie takeOff, pilotEject usw. erwähnte. Und das hat mich wirklich als Beispiel aus der Praxis angeklickt, warum Schnittstellen nicht nur nützlich sind. aber entscheidend. Eine Schnittstelle macht ein Objekt/eine Klasse intuitiv oder vermittelt zumindest den Eindruck, dass dies der Fall ist. Eine Schnittstelle dient nicht dem Objekt oder den Daten, sondern etwas, das mit diesem Objekt interagieren muss. Die klassischen Beispiele für Frucht-> Apfel-> Fuji oder Form-> Dreieck-> Gleichseitige Vererbung sind ein hervorragendes Modell für das taxonomische Verständnis eines bestimmten Objekts anhand seiner Nachkommen. Es informiert den Verbraucher und die Verarbeiter über seine allgemeinen Eigenschaften, Verhaltensweisen, ob das Objekt eine Gruppe von Dingen ist, wird Ihr System abspritzen, wenn Sie es an der falschen Stelle platzieren, oder beschreibt sensorische Daten, einen Konnektor zu einem bestimmten Datenspeicher oder Finanzdaten für die Gehaltsabrechnung erforderlich.

Ein bestimmtes Flugzeugobjekt kann eine Methode für eine Notlandung haben oder auch nicht, aber ich werde sauer sein, wenn ich davon ausgehe, dass sein Notfallland wie jedes andere flugfähige Objekt nur in DumbPlane abgedeckt aufwacht und erfährt, dass die Entwickler sich für quickLand entschieden haben weil sie dachten, dass es egal sei. Genauso wie ich frustriert wäre, wenn jeder Schraubenhersteller seine eigene Interpretation von Righty Tighty hätte oder wenn mein Fernseher nicht die Taste zum Erhöhen der Lautstärke über der Taste zum Verringern der Lautstärke hätte.

Abstrakte Klassen sind das Modell, das festlegt, was ein nachkommendes Objekt haben muss, um sich als diese Klasse zu qualifizieren. Wenn Sie nicht alle Eigenschaften einer Ente haben, spielt es keine Rolle, dass Sie die IQuack-Oberfläche implementiert haben, Sie sind nur ein seltsamer Pinguin. Schnittstellen sind die Dinge, die Sinn machen, auch wenn Sie sich über nichts anderes sicher sind. Jeff Goldblum und Starbuck konnten beide außerirdische Raumschiffe fliegen, da die Schnittstelle zuverlässig ähnlich war.

Dritte:

Ich stimme Ihrem Kollegen zu, da Sie manchmal bestimmte Methoden gleich zu Beginn durchsetzen müssen. Wenn Sie ein Active Record ORM erstellen, ist eine Speichermethode erforderlich. Dies liegt nicht an der Unterklasse, die instanziiert werden kann. Und wenn die ICRUD-Schnittstelle portabel genug ist, um nicht ausschließlich an die eine abstrakte Klasse gekoppelt zu werden, kann sie von anderen Klassen implementiert werden, um sie für jeden zuverlässig und intuitiv zu machen, der bereits mit einer der untergeordneten Klassen dieser abstrakten Klasse vertraut ist.

Auf der anderen Seite gab es früher ein gutes Beispiel dafür, wann man nicht zum Binden einer Schnittstelle an die abstrakte Klasse springen sollte, da nicht alle Listentypen eine Warteschlangenschnittstelle implementieren (oder sollten). Sie sagten, dass dieses Szenario die halbe Zeit passiert, was bedeutet, dass Sie und Ihr Mitarbeiter die halbe Zeit falsch liegen. Daher ist es am besten, zu streiten, zu debattieren, zu überlegen und, wenn sie sich als richtig herausstellen, die Kopplung anzuerkennen und zu akzeptieren . Aber werden Sie kein Entwickler, der einer Philosophie folgt, auch wenn diese nicht die beste für den jeweiligen Job ist.

1
Anthony

Denken Sie zuerst daran, was eine abstrakte Basisklasse ist und was eine Schnittstelle ist. Überlegen Sie, wann Sie das eine oder das 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 (Unterschied zwischen den beiden ist ??).

Eine abstrakte Basisklasse bietet Ihnen also etwas, was Schnittstellen 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, nicht beispielsweise für eine Schnittstelle).

Eine häufige Verwendung hierfür ist beispielsweise IDisposable. Die abstrakte Klasse implementiert die Dispose-Methode von IDisposable. Dies bedeutet, dass jede abgeleitete Version der Basisklasse automatisch verfügbar ist. 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 dies nicht der Fall ist, oder machen Sie sie weder virtuell noch abstrakt, und lassen Sie sie virtuelle Methoden aufrufen, die beispielsweise als BeforeDispose, AfterDispose, OnDispose oder Disposing bezeichnet werden.

Jedes Mal, wenn alle abgeleiteten Klassen die Schnittstelle unterstützen müssen, wird sie auf die Basisklasse übertragen. 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.

1
Ian

Es gibt noch einen weiteren Aspekt, warum der DbWorkerIFooWorker implementieren sollte, wie Sie vorschlagen.

Im Falle des Stils Ihres Kollegen verliert DbWorker die Schnittstelle BaseWorker, wenn aus irgendeinem Grund ein späteres Refactoring erfolgt und der DbWorker die abstrakte Klasse IFooWorker nicht erweitert. Dies könnte, muss aber nicht unbedingt Auswirkungen auf Clients haben, die es konsumieren, wenn sie die Schnittstelle IFooWorker erwarten.

0