Iteratormuster - warum ist es wichtig, die interne Repräsentation nicht freizulegen?


24

Ich lese C # Design Pattern Essentials . Ich lese gerade über das Iteratormuster.

Ich verstehe die Implementierung vollständig, verstehe aber nicht die Wichtigkeit oder sehe keinen Anwendungsfall. In dem Buch wird ein Beispiel gegeben, in dem jemand eine Liste von Objekten abrufen muss. Sie hätten dies tun können, indem sie ein öffentliches Eigentum, wie zum Beispiel IList<T>oder ein Array.

Das Buch schreibt

Das Problem dabei ist, dass die interne Repräsentation in beiden Klassen externen Projekten ausgesetzt war.

Was ist die interne Darstellung? Die Tatsache, dass es einarray oder IList<T>? Ich verstehe wirklich nicht, warum dies für den Verbraucher (den Programmierer, der dies nennt) eine schlechte Sache ist, zu wissen ...

Das Buch sagt dann, dass dieses Muster funktioniert, indem es sein herausstellt GetEnumerator Funktion , so dass wir GetEnumerator()die 'Liste' auf diese Weise aufrufen und aufdecken können.

Ich gehe davon aus, dass dieses Muster (wie alle) in bestimmten Situationen einen Platz hat, aber ich sehe nicht, wo und wann.



4
Da die Implementierung möglicherweise von der Verwendung eines Arrays zur Verwendung einer verknüpften Liste wechseln möchte, ohne dass Änderungen in der Consumer-Klasse erforderlich sind. Dies wäre unmöglich, wenn der Verbraucher die genaue zurückgegebene Klasse kennt.
MTilsted

11
Es ist keine schlechte Sache für den Verbraucher zu wissen , es ist eine schlechte Sache für den Verbraucher, sich darauf zu verlassen . Ich mag den Begriff "Ausblenden von Informationen" nicht, weil Sie glauben, dass die Informationen "privat" wie Ihr Passwort und nicht privat wie das Innere eines Telefons sind. Die inneren Komponenten sind verborgen, weil sie für den Benutzer irrelevant sind, nicht weil sie eine Art Geheimnis sind. Alles, was Sie wissen müssen, ist hier wählen, hier sprechen, hier zuhören.
Captain Man

Nehmen wir an, dass wir eine schöne „Schnittstelle“ haben , Fdas gibt mir Wissen (Methoden) von a, bund c. Alles ist gut und schön, es kann viele Dinge geben, die anders sind, aber nur Ffür mich. Stellen Sie sich die Methode als "Einschränkungen" oder Klauseln eines Vertrags vor F, zu denen Klassen verpflichtet sind. Nehmen wir an, wir fügen ein dzwar hinzu, weil wir es brauchen. Dies fügt eine zusätzliche Einschränkung hinzu. Jedes Mal, wenn wir dies tun, legen wir den Fs mehr und mehr auf . Letztendlich (im schlimmsten Fall) kann nur eine Sache eine sein, Falso können wir es genauso gut nicht haben. Und Fes gibt so viele Einschränkungen, dass es nur einen Weg gibt, dies zu tun.
Alec Teal

@captainman, was für ein wunderbarer Kommentar. Ja, ich verstehe, warum man viele Dinge "abstrahiert", aber die Wendung beim Wissen und Verlassen ist ... Ehrlich gesagt ... Genial. Und eine wichtige Unterscheidung, über die ich erst nach dem Lesen Ihres Beitrags nachgedacht habe.
MyDaftQuestions

Antworten:


56

Software ist ein Spiel mit Versprechen und Privilegien. Es ist niemals eine gute Idee, mehr zu versprechen, als Sie liefern können, oder mehr, als Ihr Mitarbeiter benötigt.

Dies gilt insbesondere für Typen. Der Sinn des Schreibens einer iterierbaren Sammlung besteht darin, dass der Benutzer darüber iterieren kann - nicht mehr und nicht weniger. Das Aufdecken des konkreten Typs Arrayschafft normalerweise viele zusätzliche Versprechungen, z. B. dass Sie die Sammlung nach einer Funktion Ihrer Wahl sortieren können, ganz zu schweigen von der Tatsache, dass ein Normaler Arraydem Mitarbeiter wahrscheinlich erlaubt , die darin gespeicherten Daten zu ändern .

Auch wenn Sie dies für eine gute Sache halten ("Wenn der Renderer feststellt, dass die neue Exportoption fehlt, kann er sie einfach richtig patchen! Ordentlich!"), Verringert dies insgesamt die Kohärenz der Codebasis und erschwert dies Grund zu - und Code einfach zu erklären ist das oberste Ziel des Software-Engineerings.

Wenn Ihr Collaborator nun Zugriff auf eine Reihe von Dingen benötigt, damit sie garantiert keine von ihnen verpassen, implementieren Sie eine IterableSchnittstelle und legen nur die Methoden offen , die diese Schnittstelle deklariert. Auf diese Weise können Sie im nächsten Jahr, wenn Ihre Standardbibliothek eine erheblich bessere und effizientere Datenstruktur aufweist, den zugrunde liegenden Code austauschen und davon profitieren, ohne Ihren Client-Code überall zu reparieren . Es gibt andere Vorteile, nicht mehr zu versprechen, als benötigt wird, aber dieser ist so groß, dass in der Praxis keine anderen benötigt werden.


12
Exposing the concrete type Array usually creates many additional promises, e.g. that you can sort the collection by a function of your own choosing...- Brillant. Ich habe nur nicht daran gedacht. Ja, es bringt eine "Iteration" und nur eine Iteration zurück!
MyDaftQuestions

18

Das Verbergen der Implementierung ist ein Kernprinzip von OOP und eine gute Idee in allen Paradigmen, aber es ist besonders wichtig für Iteratoren (oder was auch immer sie in dieser spezifischen Sprache heißen) in Sprachen, die eine verzögerte Iteration unterstützen.

Das Problem beim Anzeigen des konkreten Typs von Iterables - oder sogar von Schnittstellen wie IList<T>- liegt nicht in den Objekten, die sie offenlegen, sondern in den Methoden, die sie verwenden. Angenommen, Sie haben eine Funktion zum Drucken einer Liste von Foos:

void PrintFoos(IList<Foo> foos)
{
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
    }
}

Sie können diese Funktion nur zum Drucken von Listen von foo verwenden - aber nur, wenn diese implementiert sind IList<Foo>

IList<Foo> foos = //.....
PrintFoos(foos);

Aber was ist, wenn Sie jedes gerade indizierte Element der Liste drucken möchten ? Sie müssen eine neue Liste erstellen:

IList<Foo> everySecondFoo = new List<T>();
bool isIndexEven = true;
foreach (foo; foos)
{
    if (isIndexEven)
    {
        everySecondFoo.Add(foo);
    }
    isIndexEven = !isIndexEven;
}
PrintFoos(everySecondFoo);

Das ist ziemlich lang, aber mit LINQ können wir es mit einem Einzeiler machen, der eigentlich besser lesbar ist:

PrintFoos(foos.Where((foo, i) => i % 2 == 0).ToList());

Nun, haben Sie das .ToList()am Ende bemerkt ? Dadurch wird die verzögerte Abfrage in eine Liste konvertiert, an die wir sie übergeben können PrintFoos. Dies erfordert die Zuweisung einer zweiten Liste und zwei Übergaben der Elemente (eines in der ersten Liste, um die zweite Liste zu erstellen, und eines in der zweiten Liste, um sie zu drucken). Was ist auch, wenn wir dies haben:

void Print6Foos(IList<Foo> foos)
{
    int counter = 0;
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
        ++ counter;
        if (6 < counter)
        {
            return;
        }
    }
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0).ToList());

Was ist, wenn fooses Tausende von Einträgen gibt? Wir müssen sie alle durchgehen und eine riesige Liste aufteilen, um nur 6 davon zu drucken!

Enter Enumerators - C # -Version von The Iterator Pattern. Anstatt unsere Funktion eine Liste akzeptieren zu lassen, lassen wir sie akzeptieren Enumerable:

void Print6Foos(Enumerable<Foo> foos)
{
    // everything else stays the same
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0));

Jetzt Print6Fooskönnen Sie die ersten 6 Elemente der Liste träge durchlaufen, ohne den Rest der Liste berühren zu müssen.

Die interne Repräsentation nicht preiszugeben, ist hier der Schlüssel. Wenn Print6Fooseine Liste akzeptiert wurde, mussten wir ihr eine Liste geben - etwas, das den wahlfreien Zugriff unterstützt - und deshalb mussten wir eine Liste zuweisen, da die Signatur nicht garantiert, dass sie nur darüber iteriert. Durch Ausblenden der Implementierung können wir auf einfache Weise ein effizienteres EnumerableObjekt erstellen , das nur das unterstützt, was die Funktion tatsächlich benötigt.


6

Die Darstellung der internen Repräsentation ist so gut wie nie eine gute Idee. Dies macht es nicht nur schwieriger, über den Code nachzudenken, sondern auch schwieriger, ihn zu warten. Stellen Sie sich vor, Sie haben eine interne Repräsentation ausgewählt - sagen wir eine IList<T>- und diese interne aufgedeckt. Jeder, der Ihren Code verwendet, kann auf die Liste zugreifen und sich darauf verlassen, dass die interne Darstellung eine Liste ist.

Aus welchem ​​Grund auch immer Sie sich entscheiden, die interne Darstellung zu IAmWhatever<T>einem späteren Zeitpunkt zu ändern . Stattdessen müssen Sie beim einfachen Ändern der Interna Ihrer Klasse jede Codezeile und Methode ändern, wobei Sie sich darauf verlassen müssen, dass die interne Darstellung vom Typ ist IList<T>. Dies ist umständlich und bestenfalls fehleranfällig, wenn Sie der Einzige sind, der die Klasse verwendet, es kann jedoch zu Fehlern im Code anderer Personen mit Ihrer Klasse kommen. Wenn Sie nur eine Reihe von öffentlichen Methoden verfügbar gemacht haben, ohne Interna verfügbar zu machen, können Sie die interne Repräsentation ändern, ohne dass Codezeilen außerhalb Ihrer Klasse davon Notiz nehmen und so arbeiten, als ob sich nichts geändert hätte.

Aus diesem Grund ist die Kapselung einer der wichtigsten Aspekte eines nicht-trivialen Software-Designs.


6

Je weniger Sie sagen, desto weniger müssen Sie sagen.

Je weniger Sie fragen, desto weniger müssen Sie informiert werden.

Wenn Ihr Code nur verfügbar macht IEnumerable<T>, was nur unterstützt GetEnumrator(), kann er durch jeden anderen Code ersetzt werden, der unterstützt IEnumerable<T>. Dies erhöht die Flexibilität.

Wenn Ihr Code nur verwendet IEnumerable<T>, kann er von jedem implementierten Code unterstützt werden IEnumerable<T>. Auch hier gibt es zusätzliche Flexibilität.

Alle linq-to-objects zum Beispiel hängen nur von ab IEnumerable<T>. Während es einige bestimmte Implementierungen davon IEnumerable<T>beschleunigt, geschieht dies auf eine Test-and-Use-Art und Weise, die immer auf die reine Verwendung GetEnumerator()und die zurückgegebene IEnumerator<T>Implementierung zurückgreifen kann. Dies gibt ihm viel mehr Flexibilität, als wenn es auf Arrays oder Listen aufgebaut wäre.


Schöne klare Antwort. Sehr passend, dass Sie es kurz halten und daher Ihren Ratschlägen im ersten Satz folgen. Bravo!
Daniel Hollinrake

0

Eine Formulierung I wie zu verwenden Spiegel @CaptainMan ‚s Kommentar: ‚ Es ist in Ordnung für einen Verbraucher interne Details zu kennen Es ist schlecht für einen Kunden notwendig . Interne Details kennen‘

Wenn ein Kunde zu geben hat arrayoder IList<T>in ihrem Code, wenn diese Typen waren interne Details, die Verbraucher müssen erhalten sie richtig. Wenn sie falsch sind, kann der Verbraucher möglicherweise nicht kompilieren oder sogar falsche Ergebnisse erzielen. Dies führt zu denselben Verhaltensweisen wie die anderen Antworten von "Implementierungsdetails immer ausblenden, weil sie nicht verfügbar gemacht werden sollten", zeigt jedoch die Vorteile der Nichtveröffentlichung auf. Wenn Sie niemals ein Implementierungsdetail offenlegen, kann sich Ihr Kunde niemals selbst daran binden!

Ich mag es, in diesem Licht darüber nachzudenken, weil es das Konzept eröffnet, die Implementierung vor Bedeutungsschattierungen zu verbergen, anstatt dass ein Drakonier "immer Ihre Implementierungsdetails verstecken" muss. Es gibt viele Situationen, in denen die Offenlegung der Implementierung absolut gültig ist. Betrachten Sie den Fall von Echtzeit-Gerätetreibern, bei denen die Fähigkeit, über Abstraktionsschichten hinweg zu springen und Bytes an die richtigen Stellen zu verschieben, den Unterschied zwischen dem Timing und dem Nicht-Timing ausmachen kann. Oder stellen Sie sich eine Entwicklungsumgebung vor, in der Sie keine Zeit haben, um "gute APIs" zu erstellen, und die Sie sofort einsetzen müssen. Das Offenlegen zugrunde liegender APIs in solchen Umgebungen kann häufig den Unterschied zwischen einer Deadline und einer Nicht-Deadline ausmachen.

Wenn Sie diese Sonderfälle jedoch hinter sich lassen und die allgemeineren Fälle betrachten, wird es immer wichtiger, die Implementierung zu verbergen. Implementierungsdetails preiszugeben ist wie ein Strick. Bei richtiger Anwendung können Sie damit alle möglichen Dinge tun, z. B. hohe Berge erklimmen. Missbräuchlich verwendet, und es kann Sie in eine Schlinge binden. Je weniger Sie über die Verwendung Ihres Produkts wissen, desto sorgfältiger müssen Sie vorgehen.

Die Fallstudie, die ich immer wieder sehe, ist eine, in der ein Entwickler eine API erstellt, die nicht sehr sorgfältig darauf achtet, Implementierungsdetails zu verbergen. Ein zweiter Entwickler, der diese Bibliothek verwendet, stellt fest, dass der API einige wichtige Funktionen fehlen, die er haben möchte. Anstatt eine Feature-Anfrage zu schreiben (die eine Bearbeitungszeit von Monaten haben könnte), entscheiden sie sich stattdessen, ihren Zugriff auf die verborgenen Implementierungsdetails zu missbrauchen (was Sekunden dauert). Das ist an und für sich nicht schlecht, aber oft plante der API-Entwickler nie, dass dies Teil der API sein sollte. Wenn sie später die API verbessern und diese zugrunde liegenden Details ändern, stellen sie fest, dass sie an die alte Implementierung gebunden sind, da sie von jemandem als Teil der API verwendet wurden. In der schlimmsten Version wurde Ihre API verwendet, um eine kundenorientierte Funktion bereitzustellen, und jetzt ist Ihr Unternehmen s Gehaltsscheck ist an diese fehlerhafte API gebunden! Das bloße Offenlegen von Implementierungsdetails, wenn Sie nicht genug über Ihre Kunden wissen, hat sich als ausreichend erwiesen, um eine Schlinge um Ihre API zu binden!


1
Betreff: "Oder stellen Sie sich eine Entwicklungsumgebung vor, in der Sie keine Zeit haben, gute APIs zu erstellen, und die Sie sofort nutzen müssen.": Wenn Sie sich in einer solchen Umgebung befinden und aus irgendeinem Grund nicht beenden möchten ( oder Sie sind sich nicht sicher, ob Sie dies tun), empfehle ich das Buch Todesmarsch: Der komplette Softwareentwickler-Leitfaden zum Überleben von 'Mission Impossible'-Projekten . Es hat ausgezeichnete Ratschläge zu diesem Thema. (Außerdem empfehle ich Ihnen, Ihre Meinung zu ändern, damit Sie nicht
aufhören

@ruakh Ich habe festgestellt, dass es immer wichtig ist, zu verstehen, wie man in einer Umgebung arbeitet, in der man keine Zeit hat, um gute APIs zu erstellen. Um ehrlich zu sein, so sehr wir den Elfenbeinturm-Ansatz auch lieben, der echte Code ist das, was die Rechnungen tatsächlich bezahlt. Je früher wir das zugeben können, desto eher können wir uns der Software als Balance nähern und die API-Qualität mit dem produktiven Code in Einklang bringen, um sie an unsere genaue Umgebung anzupassen. Ich habe zu viele junge Entwickler gesehen, die in einem Durcheinander von Idealen gefangen sind und nicht realisiert haben, dass sie sie biegen müssen. (Ich war auch der junge Entwickler, der sich immer noch von 5 Jahren "guter API" erholt hat.)
Cort Ammon - Monica

1
Echter Code bezahlt die Rechnungen, ja. Durch ein gutes API-Design stellen Sie sicher, dass Sie weiterhin echten Code generieren können. Crappy Code macht sich nicht mehr bezahlt, wenn ein Projekt nicht mehr zu warten ist und es unmöglich wird, Fehler zu beheben, ohne neue zu erstellen. (Und auf jeden
Fall

1
@ruakh Ich habe festgestellt, dass es möglich ist, guten Code ohne "gute APIs" zu erstellen. Wenn man die Gelegenheit dazu hat, sind gute APIs nett, aber sie als Notwendigkeit zu behandeln, verursacht mehr Streit, als es wert ist. Bedenken Sie: Die API für den menschlichen Körper ist schrecklich, aber wir denken immer noch, dass es ziemlich gute kleine Ingenieurleistungen sind =)
Cort Ammon - Reinstate Monica

Das andere Beispiel, das ich geben würde, war die Inspiration für das Argument, Timing zu machen. Eine der Gruppen, mit denen ich zusammenarbeite, hatte einige Gerätetreiber, die sie entwickeln mussten, mit harten Echtzeitanforderungen. Sie hatten 2 APIs zu arbeiten. Einer war gut, einer weniger. Die gute API konnte kein Timing erzielen, da sie die Implementierung verbarg. Die niedrigere API hat die Implementierung verfügbar gemacht, sodass Sie prozessorspezifische Techniken verwenden können, um ein funktionsfähiges Produkt zu erstellen. Die gute API wurde höflich auf den Markt gebracht und sie erfüllten weiterhin die Timing-Anforderungen.
Cort Ammon - Reinstate Monica

0

Sie müssen nicht unbedingt wissen, wie die interne Darstellung Ihrer Daten aussieht - oder in Zukunft aussehen wird.

Angenommen, Sie haben eine Datenquelle, die Sie als Array darstellen, und können diese mit einer regulären for-Schleife durchlaufen.

Sagen Sie jetzt, Sie möchten umgestalten. Ihr Chef hat darum gebeten, dass die Datenquelle ein Datenbankobjekt ist. Darüber hinaus möchte er die Möglichkeit haben, Daten von einer API, einer Hashmap, einer verknüpften Liste, einer sich drehenden Magnettrommel, einem Mikrofon oder einer Echtzeitzählung der in der äußeren Mongolei gefundenen Yak-Späne abzurufen.

Nun, wenn Sie nur eine for-Schleife haben, können Sie Ihren Code einfach ändern, um auf das Datenobjekt so zuzugreifen, wie es erwartet wird.

Wenn Sie 1000 Klassen haben, die versuchen, auf das Datenobjekt zuzugreifen, haben Sie jetzt ein großes Refactoring-Problem.

Ein Iterator ist eine Standardmethode für den Zugriff auf eine listenbasierte Datenquelle. Es ist eine gemeinsame Schnittstelle. Durch die Verwendung wird Ihre Code-Datenquelle agnostisch.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.