Ich bin gerade dabei, C # zu beherrschen, also lese ich Adaptive Code über C # von Gary McLean Hall .
Er schreibt über Muster und Anti-Muster. In dem Teil Implementierungen versus Schnittstellen schreibt er Folgendes:
Entwickler, die mit dem Konzept der Programmierung von Schnittstellen noch nicht vertraut sind, haben häufig Schwierigkeiten, die Hintergründe der Schnittstelle loszulassen.
Zum Zeitpunkt der Kompilierung sollte jeder Client einer Schnittstelle keine Ahnung haben, welche Implementierung der Schnittstelle er verwendet. Dieses Wissen kann zu falschen Annahmen führen, die den Client an eine bestimmte Implementierung der Schnittstelle koppeln.
Stellen Sie sich das allgemeine Beispiel vor, in dem eine Klasse einen Datensatz in einem dauerhaften Speicher speichern muss. Zu diesem Zweck delegiert es zu Recht an eine Schnittstelle, die die Details des verwendeten dauerhaften Speichermechanismus verbirgt. Es wäre jedoch nicht richtig, Annahmen darüber zu treffen, welche Implementierung der Schnittstelle zur Laufzeit verwendet wird. Das Umwandeln des Schnittstellenverweises in eine Implementierung ist beispielsweise immer eine schlechte Idee.
Es mag die Sprachbarriere sein oder mein Mangel an Erfahrung, aber ich verstehe nicht ganz, was das bedeutet. Folgendes verstehe ich:
Ich habe ein Freizeit-Spaßprojekt, um C # zu üben. Dort habe ich eine Klasse:
public class SomeClass...
Diese Klasse wird an vielen Orten verwendet. Während ich C # lernte, las ich, dass es besser ist, mit einer Schnittstelle zu abstrahieren, also habe ich Folgendes gemacht
public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.
public class SomeClass : ISomeClass <- Same as before. All implementation here.
Also habe ich alle Klassenreferenzen durch ISomeClass ersetzt.
Außer in der Konstruktion, wo ich schrieb:
ISomeClass myClass = new SomeClass();
Verstehe ich richtig, dass dies falsch ist? Wenn ja, warum und was soll ich stattdessen tun?
quelle
ISomeClass myClass = new SomeClass();
? Wenn du das wirklich meinst, ist das Rekursion im Konstruktor, wahrscheinlich nicht das, was du willst ?ISomeClass
), aber es ist auch einfach, zu allgemeine Schnittstellen zu erstellen, für die es unmöglich ist , nützlichen Code zu schreiben sind die Schnittstelle zu überdenken und den Code umzuschreiben oder zu downcasten.Antworten:
Das Abstrahieren Ihrer Klasse in eine Schnittstelle sollte nur dann in Betracht gezogen werden, wenn Sie beabsichtigen, andere Implementierungen dieser Schnittstelle zu schreiben, oder wenn die starke Möglichkeit besteht, dies in Zukunft zu tun.
Also vielleicht
SomeClass
undISomeClass
ist ein schlechtes Beispiel, weil es wie eineOracleObjectSerializer
Klasse und eineIOracleObjectSerializer
Schnittstelle wäre.Ein genaueres Beispiel wäre so etwas wie
OracleObjectSerializer
und aIObjectSerializer
. Der einzige Ort in Ihrem Programm, an dem Sie sich für die zu verwendende Implementierung interessieren, ist der Zeitpunkt, an dem die Instanz erstellt wird. Manchmal wird dies durch die Verwendung eines Fabrikmusters weiter entkoppelt.Überall in Ihrem Programm sollte
IObjectSerializer
es egal sein, wie es funktioniert. Nehmen wir für eine Sekunde an, dass Sie zusätzlich zu auch eineSQLServerObjectSerializer
Implementierung habenOracleObjectSerializer
. Angenommen, Sie müssen eine spezielle Eigenschaft festlegen, die festgelegt werden soll, und diese Methode ist nur in OracleObjectSerializer und nicht in SQLServerObjectSerializer vorhanden.Es gibt zwei Möglichkeiten: den falschen Weg und den Ansatz des Liskov-Substitutionsprinzips .
Der falsche Weg
Die falsche Methode und genau die Instanz, auf die in Ihrem Buch verwiesen wird, besteht darin, eine Instanz von zu nehmen
IObjectSerializer
und sie inOracleObjectSerializer
die MethodesetProperty
umzuwandeln und dann die Methode aufzurufen, die nur für verfügbar istOracleObjectSerializer
. Das ist schlecht, weil Sie , obwohl Sie vielleicht wissen, dass eine Instanz eine istOracleObjectSerializer
, einen weiteren Punkt in Ihrem Programm einführen, an dem Sie wissen möchten, um welche Implementierung es sich handelt. Wenn sich diese Implementierung ändert und vermutlich früher oder später, wenn Sie mehrere Implementierungen haben, müssen Sie alle diese Stellen finden und die richtigen Anpassungen vornehmen. Im schlimmsten Fall wandeln Sie eineIObjectSerializer
Instanz in ein umOracleObjectSerializer
und erhalten einen Laufzeitfehler in der Produktion.Ansatz des Liskov-Substitutionsprinzips
Liskov sagte, dass Sie niemals Methoden wie
setProperty
in der Implementierungsklasse benötigen sollten, wie im Fall von my,OracleObjectSerializer
wenn dies richtig gemacht wird. Wenn Sie eine KlasseOracleObjectSerializer
zu abstrahierenIObjectSerializer
, sollten Sie alle Methoden umfassen, die zur Verwendung dieser Klasse erforderlich sind. Wenn dies nicht möglich ist, stimmt etwas mit Ihrer Abstraktion nicht ( z. B. der Versuch, eineDog
Klasse alsIPerson
Implementierung zu verwenden).Der richtige Ansatz wäre, eine
setProperty
Methode zur Verfügung zu stellenIObjectSerializer
. Ähnliche MethodenSQLServerObjectSerializer
würden diesesetProperty
Methode idealerweise durcharbeiten. Besser noch, Sie standardisieren Eigenschaftsnamen durch eine Angabe,Enum
bei der jede Implementierung diese Aufzählung in eine Entsprechung für ihre eigene Datenbankterminologie übersetzt.Einfach gesagt, mit einem
ISomeClass
ist nur die Hälfte davon. Sie sollten es niemals außerhalb der für die Erstellung verantwortlichen Methode umwandeln müssen. Dies ist mit ziemlicher Sicherheit ein schwerwiegender Konstruktionsfehler.quelle
IObjectSerializer
zu ,OracleObjectSerializer
weil Sie „wissen“ , dass das ist , was es ist, dann sollten Sie ehrlich mit sich selbst (und was noch wichtiger ist , mit anderen , die diesen Code halten kann, was Ihre Zukunft umfassen kann Selbst), und nutzen Sie denOracleObjectSerializer
gesamten Weg von der Erstellung bis zur Verwendung. Dies macht es sehr öffentlich und klar, dass Sie eine Abhängigkeit von einer bestimmten Implementierung einführen - und die damit verbundene Arbeit und Hässlichkeit wird selbst zu einem starken Hinweis darauf, dass etwas nicht stimmt.Die akzeptierte Antwort ist richtig und sehr nützlich, aber ich möchte kurz auf die Codezeile eingehen, nach der Sie gefragt haben:
Im Großen und Ganzen ist das nicht schrecklich. Das, was nach Möglichkeit vermieden werden sollte, wäre Folgendes:
Wenn Ihr Code extern als Schnittstelle bereitgestellt wird, diese aber intern in eine bestimmte Implementierung umwandelt, wird "Weil ich weiß, dass es nur diese Implementierung sein wird". Selbst wenn dies der Fall sein sollte, geben Sie freiwillig die echte Typensicherheit auf, indem Sie eine Schnittstelle verwenden und auf eine Implementierung umwandeln, nur um so zu tun, als könnten Sie die Abstraktion verwenden. Wenn jemand anderes später an dem Code arbeiten und eine Methode finden sollte, die einen Schnittstellenparameter akzeptiert, wird davon ausgegangen, dass jede Implementierung dieser Schnittstelle eine gültige Option für die Übergabe ist Linie, nachdem Sie vergessen haben, dass eine bestimmte Methode darüber liegt, welche Parameter es benötigt. Wenn Sie jemals das Bedürfnis verspüren, von einer Schnittstelle zu einer bestimmten Implementierung zu wechseln, können Sie entweder die Schnittstelle, die Implementierung, oder der Code, der auf sie verweist, ist falsch entworfen und sollte sich ändern. Wenn die Methode beispielsweise nur funktioniert, wenn das übergebene Argument eine bestimmte Klasse ist, sollte der Parameter nur diese Klasse akzeptieren.
Nun zurück zu Ihrem Konstruktoraufruf
Die Probleme aus dem Casting treffen nicht wirklich zu. Nichts davon scheint äußerlich exponiert zu sein, so dass mit ihm kein besonderes Risiko verbunden ist. Im Wesentlichen handelt es sich bei dieser Codezeile selbst um ein Implementierungsdetail, das die Schnittstellen zunächst abstrahieren sollen, sodass ein externer Beobachter die gleiche Funktionsweise sieht, unabhängig davon, was sie tun. Dies hat jedoch auch nichts mit dem Vorhandensein einer Schnittstelle zu tun. Ihr
myClass
hat den TypISomeClass
, aber es hat keinen Grund, da es immer eine bestimmte Implementierung zugeordnet ist,SomeClass
. Es gibt einige kleinere potenzielle Vorteile, z. B. die Möglichkeit, die Implementierung im Code zu tauschen, indem nur der Konstruktoraufruf geändert wird, oder diese Variable später einer anderen Implementierung zuzuweisen Die Implementierung dieses Musters lässt Ihren Code so aussehen, als würden Schnittstellen nur auswendig verwendet, nicht aus dem tatsächlichen Verständnis der Vorteile von Schnittstellen heraus.quelle
Ich denke, es ist einfacher, Ihren Code mit einem schlechten Beispiel zu zeigen:
Das Problem ist, dass es beim ersten Schreiben des Codes wahrscheinlich nur eine Implementierung dieser Schnittstelle gibt, sodass das Casting weiterhin funktionieren würde. In Zukunft können Sie lediglich eine andere Klasse implementieren und dann (wie in meinem Beispiel gezeigt) Versuchen Sie, auf Daten zuzugreifen, die in dem von Ihnen verwendeten Objekt nicht vorhanden sind.
quelle
Definieren wir aus Gründen der Übersichtlichkeit das Casting.
Casting bedeutet, etwas gewaltsam von einem Typ in einen anderen umzuwandeln. Ein gängiges Beispiel ist das Umwandeln einer Gleitkommazahl in einen Integer-Typ. Beim Casting kann eine bestimmte Konvertierung angegeben werden. Standardmäßig werden die Bits jedoch einfach neu interpretiert.
Hier ist ein Beispiel für die Übertragung von dieser Microsoft-Dokumentseite .
Sie könnten das Gleiche tun und etwas umwandeln, das eine Schnittstelle für eine bestimmte Implementierung dieser Schnittstelle implementiert. Dies sollte jedoch nicht geschehen, da dies zu einem Fehler oder unerwartetem Verhalten führt, wenn eine andere als die erwartete Implementierung verwendet wird.
quelle
Animal
/ würden die Bits neu interpretiert (alle Giraffe-spezifischen Daten würden als "nicht Teil dieses Objekts" interpretiert).Giraffe
Animal a = (Animal)g;
Meine 5 Cent:
Alle diese Beispiele sind in Ordnung, aber sie sind keine Beispiele aus der realen Welt und haben die Absichten der realen Welt nicht gezeigt.
Ich kenne C # nicht und werde daher ein abstraktes Beispiel geben (Mischung aus Java und C ++). Hoffe das ist OK.
Angenommen, Sie haben eine Schnittstelle
iList
:Angenommen, es gibt viele Implementierungen:
Man kann sich viele verschiedene Implementierungen vorstellen.
Angenommen, wir haben folgenden Code:
Es zeigt deutlich unsere Absicht, die wir nutzen wollen
iList
. Sicher, wir sind nicht mehr in der Lage,DynamicArrayList
bestimmte Operationen durchzuführen, aber wir brauchen eineiList
.Betrachten Sie folgenden Code:
Jetzt wissen wir nicht einmal, was die Implementierung ist. Dieses letzte Beispiel wird häufig in der Bildverarbeitung verwendet, wenn Sie eine Datei von der Festplatte laden und den Dateityp (gif, jpeg, png, bmp ...) nicht benötigen. skalieren, am Ende als PNG speichern).
quelle
Sie haben eine Schnittstelle ISomeClass und ein Objekt myObject, von dem Sie nichts aus Ihrem Code wissen, außer dass es für die Implementierung von ISomeClass deklariert ist.
Sie haben eine Klasse SomeClass, von der Sie wissen, dass sie das Interface ISomeClass implementiert. Sie wissen das, weil es für die Implementierung von ISomeClass deklariert wurde, oder Sie haben es selbst implementiert, um ISomeClass zu implementieren.
Was ist falsch daran, myClass in SomeClass umzuwandeln? Zwei Dinge sind falsch. Erstens wissen Sie nicht wirklich, dass myClass in SomeClass (eine Instanz von SomeClass oder eine Unterklasse von SomeClass) konvertiert werden kann, sodass die Besetzung schief gehen kann. Zweitens sollten Sie das nicht tun müssen. Sie sollten mit myClass arbeiten, das als iSomeClass deklariert ist, und ISomeClass-Methoden verwenden.
Der Punkt, an dem Sie ein SomeClass-Objekt erhalten, ist, wenn eine Schnittstellenmethode aufgerufen wird. Irgendwann rufen Sie myClass.myMethod () auf, das in der Schnittstelle deklariert ist, aber eine Implementierung in SomeClass hat, und möglicherweise auch in vielen anderen Klassen, die ISomeClass implementieren. Wenn ein Aufruf in Ihrem SomeClass.myMethod-Code endet, wissen Sie, dass self eine Instanz von SomeClass ist, und zu diesem Zeitpunkt ist es absolut in Ordnung und richtig, ihn als SomeClass-Objekt zu verwenden. Wenn es sich tatsächlich um eine Instanz von OtherClass und nicht um SomeClass handelt, gelangen Sie natürlich nicht zum SomeClass-Code.
quelle