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.
quelle
F
das gibt mir Wissen (Methoden) vona
,b
undc
. Alles ist gut und schön, es kann viele Dinge geben, die anders sind, aber nurF
für mich. Stellen Sie sich die Methode als "Einschränkungen" oder Klauseln eines Vertrags vorF
, zu denen Klassen verpflichtet sind. Nehmen wir an, wir fügen eind
zwar hinzu, weil wir es brauchen. Dies fügt eine zusätzliche Einschränkung hinzu. Jedes Mal, wenn wir dies tun, legen wir denF
s mehr und mehr auf . Letztendlich (im schlimmsten Fall) kann nur eine Sache eine sein,F
also können wir es genauso gut nicht haben. UndF
es gibt so viele Einschränkungen, dass es nur einen Weg gibt, dies zu tun.Antworten:
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
Array
schafft 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 NormalerArray
dem 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
Iterable
Schnittstelle 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.quelle
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!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 vonFoo
s:Sie können diese Funktion nur zum Drucken von Listen von foo verwenden - aber nur, wenn diese implementiert sind
IList<Foo>
Aber was ist, wenn Sie jedes gerade indizierte Element der Liste drucken möchten ? Sie müssen eine neue Liste erstellen:
Das ist ziemlich lang, aber mit LINQ können wir es mit einem Einzeiler machen, der eigentlich besser lesbar ist:
Nun, haben Sie das
.ToList()
am Ende bemerkt ? Dadurch wird die verzögerte Abfrage in eine Liste konvertiert, an die wir sie übergeben könnenPrintFoos
. 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:Was ist, wenn
foos
es 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
:Jetzt
Print6Foos
kö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
Print6Foos
eine 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 effizienteresEnumerable
Objekt erstellen , das nur das unterstützt, was die Funktion tatsächlich benötigt.quelle
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 istIList<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.
quelle
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ütztGetEnumrator()
, kann er durch jeden anderen Code ersetzt werden, der unterstütztIEnumerable<T>
. Dies erhöht die Flexibilität.Wenn Ihr Code nur verwendet
IEnumerable<T>
, kann er von jedem implementierten Code unterstützt werdenIEnumerable<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 davonIEnumerable<T>
beschleunigt, geschieht dies auf eine Test-and-Use-Art und Weise, die immer auf die reine VerwendungGetEnumerator()
und die zurückgegebeneIEnumerator<T>
Implementierung zurückgreifen kann. Dies gibt ihm viel mehr Flexibilität, als wenn es auf Arrays oder Listen aufgebaut wäre.quelle
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
array
oderIList<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!
quelle
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.
quelle