Was bedeutet der Autor, wenn der Schnittstellenverweis auf eine Implementierung umgewandelt wird?

17

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?

Marshall
quelle
25
Nirgendwo in Ihrem Beispiel wandeln Sie ein Objekt vom Typ Schnittstelle in den Implementierungstyp um. Sie weisen einer Schnittstellenvariablen etwas vom Implementierungstyp zu, was vollkommen in Ordnung und korrekt ist.
Caleth
1
Was meinst du mit "in dem Konstruktor, in dem ich geschrieben habe ISomeClass myClass = new SomeClass();? Wenn du das wirklich meinst, ist das Rekursion im Konstruktor, wahrscheinlich nicht das, was du willst ?
Erik Eidt
@ Erik: Ja. Im Aufbau. Du hast Recht. Wird die Frage korrigieren. Vielen Dank
Marshall
Unterhaltsame Tatsache: F # hat in dieser Hinsicht eine bessere Geschichte als C # - es beseitigt implizite Schnittstellenimplementierungen. Wenn Sie also eine Schnittstellenmethode aufrufen möchten, müssen Sie auf den Schnittstellentyp upcasten. Dadurch wird sehr deutlich, wann und wie Sie Schnittstellen in Ihrem Code verwenden, und die Programmierung von Schnittstellen ist in der Sprache viel besser verankert.
Scrwtp
3
Dies ist ein wenig vom Thema abweichend, aber ich denke, der Autor hat das Problem falsch diagnostiziert, das Menschen mit neuem Konzept haben. Meiner Meinung nach besteht das Problem darin, dass Leute, die neu im Konzept sind, nicht wissen, wie man gute Schnittstellen erstellt. Es ist sehr einfach, zu spezifische Schnittstellen zu erstellen, die eigentlich keine Allgemeingültigkeit bieten (was durchaus passieren kann 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.
Derek Elkins verließ SE

Antworten:

37

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 SomeClassund ISomeClassist ein schlechtes Beispiel, weil es wie eine OracleObjectSerializerKlasse und eine IOracleObjectSerializerSchnittstelle wäre.

Ein genaueres Beispiel wäre so etwas wie OracleObjectSerializerund a IObjectSerializer. 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 IObjectSerializeres egal sein, wie es funktioniert. Nehmen wir für eine Sekunde an, dass Sie zusätzlich zu auch eine SQLServerObjectSerializerImplementierung haben OracleObjectSerializer. 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 IObjectSerializerund sie in OracleObjectSerializerdie Methode setPropertyumzuwandeln und dann die Methode aufzurufen, die nur für verfügbar ist OracleObjectSerializer. Das ist schlecht, weil Sie , obwohl Sie vielleicht wissen, dass eine Instanz eine ist OracleObjectSerializer, 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 eine IObjectSerializerInstanz in ein um OracleObjectSerializerund erhalten einen Laufzeitfehler in der Produktion.

Ansatz des Liskov-Substitutionsprinzips

Liskov sagte, dass Sie niemals Methoden wie setPropertyin der Implementierungsklasse benötigen sollten, wie im Fall von my, OracleObjectSerializerwenn dies richtig gemacht wird. Wenn Sie eine Klasse OracleObjectSerializerzu abstrahieren IObjectSerializer, 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, eine DogKlasse als IPersonImplementierung zu verwenden).

Der richtige Ansatz wäre, eine setPropertyMethode zur Verfügung zu stellen IObjectSerializer. Ähnliche Methoden SQLServerObjectSerializerwürden diese setPropertyMethode idealerweise durcharbeiten. Besser noch, Sie standardisieren Eigenschaftsnamen durch eine Angabe, Enumbei der jede Implementierung diese Aufzählung in eine Entsprechung für ihre eigene Datenbankterminologie übersetzt.

Einfach gesagt, mit einem ISomeClassist 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.

Neil
quelle
1
Es scheint mir , dass, wenn Sie gegossen werden IObjectSerializerzu , OracleObjectSerializerweil 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 den OracleObjectSerializergesamten 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.
KRyan
(Und wenn Sie aus irgendeinem Grund wirklich tun auf eine bestimmte Implementierung verlassen müssen, ist es viel klarer wird , dass das ist , was Sie tun und dass Sie es mit Absicht tun und Zweck. Diese „sollte“ nie natürlich passieren, und in 99% der Fälle scheint es, als ob es passiert, ist es tatsächlich nicht und Sie sollten Dinge reparieren, aber nichts ist jemals 100% sicher oder so, wie die Dinge sein sollten.)
KRyan
@KRyan Auf jeden Fall. Abstraktion sollte nur verwendet werden, wenn Sie eine Notwendigkeit dafür haben. Die Verwendung von Abstraktion, wenn dies nicht erforderlich ist, dient nur dazu, den Code etwas schwieriger verständlich zu machen.
Neil
29

Die akzeptierte Antwort ist richtig und sehr nützlich, aber ich möchte kurz auf die Codezeile eingehen, nach der Sie gefragt haben:

ISomeClass myClass = new SomeClass();

Im Großen und Ganzen ist das nicht schrecklich. Das, was nach Möglichkeit vermieden werden sollte, wäre Folgendes:

void someMethod(ISomeClass interface){
    SomeClass cast = (SomeClass)interface;
}

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

ISomeClass myClass = new SomeClass();

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 myClasshat den Typ ISomeClass, 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.

Kamil Drakari
quelle
1
Apache Math macht das mit Cartesian3D Source, Zeile 286 , was sehr ärgerlich sein kann.
J_F_B_M
1
Dies ist die tatsächlich richtige Antwort auf die ursprüngliche Frage.
Benjamin Gruenbaum
2

Ich denke, es ist einfacher, Ihren Code mit einem schlechten Beispiel zu zeigen:

public interface ISomeClass
{
    void DoThing();
}

public class SomeClass : ISomeClass
{
    public void DoThing()
    {
       // Mine for BitCoin
    }

}

public class AnotherClass : ISomeClass
{
    public void DoThing()
    {
        // Mine for oil
    }
    public Decimal Depth;
 }

 void main()
 {
     ISomeClass task = new SomeClass();

     task.DoThing(); //  This is good

     Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
 }

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.

Neil
quelle
Warum hallo, Sir. Hat dir jemals jemand gesagt, wie hübsch du bist?
Neil
2

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 .

// Create a new derived type.  
Giraffe g = new Giraffe();  

// Implicit conversion to base type is safe.  
Animal a = g;  

// Explicit conversion is required to cast back  
// to derived type. Note: This will compile but will  
// throw an exception at run time if the right-side  
// object is not in fact a Giraffe.  
Giraffe g2 = (Giraffe) a;  

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.

Ryan1729
quelle
"Casting konvertiert etwas von einem Typ in einen anderen." Casting konvertiert explizit etwas von einem Typ in einen anderen. (Insbesondere ist "cast" der Name für die Syntax, mit der diese Konvertierung angegeben wird.) Implizite Konvertierungen werden nicht umgewandelt. Msgstr "Beim Umwandeln kann eine bestimmte Umwandlung angegeben werden, die Standardeinstellung ist jedoch, die Bits einfach neu zu interpretieren." -- Sicherlich nicht. Es gibt viele implizite und explizite Konvertierungen, die signifikante Änderungen an Bitmustern beinhalten.
HDV
@hvd Ich habe jetzt eine Korrektur in Bezug auf das explizite Casting vorgenommen. Als ich sagte, dass die Standardeinstellung darin besteht, die Bits einfach neu zu interpretieren, habe ich versucht auszudrücken, dass in den Fällen, in denen eine Umwandlung automatisch definiert wird, die Bits neu interpretiert werden, wenn Sie sie in einen anderen Typ umwandeln . Im obigen Beispiel Animal/ würden die Bits neu interpretiert (alle Giraffe-spezifischen Daten würden als "nicht Teil dieses Objekts" interpretiert). GiraffeAnimal a = (Animal)g;
Ryan1729
Trotz der Aussagen von hvd wird der Begriff "Besetzung" häufig für implizite Konvertierungen verwendet. Siehe z . B. https://www.google.com/search?q="implicit+cast"&tbm=bks . Technisch ist es meiner Meinung nach richtiger, den Begriff "Besetzung" für explizite Konvertierungen zu reservieren, sofern Sie nicht verwirrt sind, wenn andere ihn anders verwenden.
Ruakh
0

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:

interface iList<Key,Value>{
   bool add(Key k, Value v);
   bool remove(Element e);
   Value get(Key k);
}

Angenommen, es gibt viele Implementierungen:

  • DynamicArrayList - verwendet ein flaches Array, das am Ende schnell eingefügt und entfernt werden kann.
  • LinkedList - verwendet eine doppelt verknüpfte Liste, die schnell vor und nach dem Ende eingefügt werden kann.
  • AVLTreeList - verwendet AVL Tree, um schnell alles zu erledigen, benötigt aber viel Speicher
  • SkipList - verwendet SkipList, um alles schnell zu erledigen, langsamer als AVL Tree, benötigt aber weniger Speicher.
  • HashList - verwendet HashTable

Man kann sich viele verschiedene Implementierungen vorstellen.

Angenommen, wir haben folgenden Code:

uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);

Es zeigt deutlich unsere Absicht, die wir nutzen wollen iList. Sicher, wir sind nicht mehr in der Lage, DynamicArrayListbestimmte Operationen durchzuführen, aber wir brauchen eine iList.

Betrachten Sie folgenden Code:

iList list = factory.getList();

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).

Nick
quelle
0

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.

gnasher729
quelle