Deklarieren Sie keine Schnittstellen für unveränderliche Objekte
[BEARBEITEN] Wenn die fraglichen Objekte Datenübertragungsobjekte (DTOs) oder einfache alte Daten (PODs) darstellen
Ist das eine vernünftige Richtlinie?
Bisher habe ich häufig Schnittstellen für versiegelte Klassen erstellt, die unveränderlich sind (Daten können nicht geändert werden). Ich habe versucht, mich selbst zu hüten, die Schnittstelle überall dort zu verwenden, wo es mir um Unveränderlichkeit geht.
Leider durchdringt die Benutzeroberfläche den Code (und ich mache mir nicht nur Sorgen um meinen Code). Sie beenden die Übergabe einer Schnittstelle und möchten diese dann an einen Code übergeben, der wirklich davon ausgehen möchte, dass das an ihn übergebene Objekt unveränderlich ist.
Aufgrund dieses Problems überlege ich mir, niemals Schnittstellen für unveränderliche Objekte zu deklarieren.
Dies könnte Konsequenzen in Bezug auf Unit-Tests haben. Aber scheint dies ansonsten eine vernünftige Richtlinie zu sein?
Oder gibt es ein anderes Muster, das ich verwenden sollte, um das Problem der "Ausbreitungsschnittstelle" zu vermeiden, das ich sehe?
(Ich verwende diese unveränderlichen Objekte aus verschiedenen Gründen: Hauptsächlich aus Gründen der Threadsicherheit, da ich viel Code mit mehreren Threads schreibe; aber auch, weil ich vermeiden kann, defensive Kopien von Objekten an Methoden zu übergeben. Code wird in diesem Fall sehr viel einfacher.) In vielen Fällen wissen Sie, dass etwas unveränderlich ist - was Sie nicht tun, wenn Sie eine Schnittstelle erhalten haben. Tatsächlich können Sie häufig nicht einmal eine defensive Kopie eines Objekts erstellen, auf das über eine Schnittstelle verwiesen wird, wenn es keine Schnittstelle bietet Klonoperation oder irgendeine Art, sie zu serialisieren ...)
[BEARBEITEN]
In diesem Blogbeitrag von Eric Lippert finden Sie weitere Informationen zu den Gründen, warum ich Objekte unveränderlich machen möchte:
http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/
Ich sollte auch darauf hinweisen, dass ich hier mit einigen Konzepten auf niedrigerer Ebene arbeite, wie z. B. Elementen, die in Jobwarteschlangen mit mehreren Threads bearbeitet / weitergegeben werden. Dies sind im Wesentlichen DTOs.
Auch Joshua Bloch empfiehlt in seinem Buch Effective Java die Verwendung unveränderlicher Objekte .
Nachverfolgen
Vielen Dank für das Feedback an alle. Ich habe mich entschlossen, diese Richtlinie für DTOs und deren Mitglieder zu verwenden. Bisher funktioniert es gut, aber es ist erst eine Woche her ... Trotzdem sieht es gut aus.
Es gibt noch einige andere Fragen, die ich dazu stellen möchte. vor allem etwas, das ich "Deep or Shallow Immutability" nenne (Nomenklatur, die ich beim Klonen von Deep and Shallow gestohlen habe) - aber das ist eine Frage für ein anderes Mal.
quelle
List<Number>
die halten kannInteger
,Float
,Long
,BigDecimal
, etc ... All das sich unveränderlich sind.Antworten:
Meiner Meinung nach ist Ihre Regel eine gute (oder zumindest keine schlechte), aber nur aufgrund der von Ihnen beschriebenen Situation. Ich würde nicht sagen, dass ich in allen Situationen damit einverstanden bin. Aus Sicht meines inneren Pedanten würde ich sagen, dass Ihre Regel technisch zu weit gefasst ist.
Normalerweise definieren Sie unveränderliche Objekte nur dann, wenn sie im Wesentlichen als Datenübertragungsobjekte (Data Transfer Objects, DTO) verwendet werden. Dies bedeutet, dass sie Dateneigenschaften, aber nur sehr wenig Logik und keine Abhängigkeiten enthalten. Wenn das der Fall ist, wie es scheint, würde ich sagen, dass Sie die konkreten Typen sicher direkt anstelle von Schnittstellen verwenden können.
Ich bin mir sicher, dass es einige Puristen geben wird, die Unit-Tests ablehnen, aber meiner Meinung nach können DTO-Klassen sicher von den Anforderungen für Unit-Tests und die Abhängigkeitsinjektion ausgeschlossen werden. Es ist nicht erforderlich, eine Factory zum Erstellen eines DTO zu verwenden, da diese keine Abhängigkeiten aufweist. Wenn alles die DTOs direkt nach Bedarf erstellt, gibt es sowieso keine Möglichkeit, einen anderen Typ zu injizieren, sodass keine Schnittstelle erforderlich ist. Und da sie keine Logik enthalten, gibt es nichts zu testen. Selbst wenn sie eine Logik enthalten, sollte es trivial sein, die Logik, falls erforderlich, einem Komponententest zu unterziehen, solange sie keine Abhängigkeiten aufweisen.
Aus diesem Grund denke ich, dass die Festlegung einer Regel, dass nicht alle DTO-Klassen eine Schnittstelle implementieren sollen, Ihrem Software-Design nicht schaden wird, auch wenn dies möglicherweise unnötig ist. Da Sie diese Anforderung haben, dass die Daten unveränderlich sein müssen und Sie dies nicht über eine Schnittstelle erzwingen können, würde ich sagen, dass es absolut legitim ist, diese Regel als Codierungsstandard festzulegen.
Das größere Problem ist jedoch die Notwendigkeit, eine saubere DTO-Ebene strikt durchzusetzen. Solange Ihre schnittstellenlosen unveränderlichen Klassen nur in der DTO-Schicht existieren und Ihre DTO-Schicht frei von Logik und Abhängigkeiten ist, sind Sie sicher. Wenn Sie jedoch anfangen, Ihre Ebenen zu mischen, und Sie über Klassen ohne Schnittstelle verfügen, die gleichzeitig als Business-Layer-Klassen fungieren, werden Sie wahrscheinlich auf große Schwierigkeiten stoßen.
quelle
Früher machte ich viel Aufhebens darum, meinen Code für Missbrauch unangreifbar zu machen. Ich habe schreibgeschützte Benutzeroberflächen erstellt, um mutierende Mitglieder auszublenden, meine generischen Signaturen stark eingeschränkt usw. Es stellte sich heraus, dass ich die meiste Zeit Entwurfsentscheidungen traf, weil ich meinen imaginären Mitarbeitern nicht vertraute. "Vielleicht stellen sie eines Tages einen neuen Einsteiger ein und er wird nicht wissen, dass Klasse XYZ DTO ABC nicht aktualisieren kann. Oh nein!" Ein anderes Mal habe ich mich auf das falsche Problem konzentriert - die offensichtliche Lösung ignoriert - den Wald nicht durch die Bäume gesehen.
Ich erstelle nie mehr Schnittstellen für meine DTOs. Ich arbeite unter der Annahme, dass die Leute, die meinen Code berühren (in erster Linie ich selbst), wissen, was erlaubt ist und was Sinn macht. Wenn ich den gleichen dummen Fehler mache, versuche ich normalerweise nicht, meine Schnittstellen zu härten. Jetzt verbringe ich die meiste Zeit damit zu verstehen, warum ich immer wieder den gleichen Fehler mache. Es ist normalerweise, weil ich etwas überanalysiere oder ein Schlüsselkonzept vermisse. Es war viel einfacher, mit meinem Code zu arbeiten, seit ich es aufgegeben habe, paranoid zu sein. Außerdem habe ich weniger "Frameworks", die das Wissen eines Insiders erforderten, um an dem System zu arbeiten.
Mein Fazit war, das Einfachste zu finden, das funktioniert. Die zusätzliche Komplexität der Erstellung sicherer Schnittstellen verschwendet nur Entwicklungszeit und kompliziert ansonsten einfachen Code. Sorgen Sie sich über solche Dinge, wenn 10.000 Entwickler Ihre Bibliotheken verwenden. Glauben Sie mir, es wird Sie vor vielen unnötigen Spannungen bewahren.
quelle
Es scheint eine gute Richtlinie zu sein, aber aus seltsamen Gründen. Ich hatte eine Reihe von Stellen, an denen eine Schnittstelle (oder eine abstrakte Basisklasse) einheitlichen Zugriff auf eine Reihe unveränderlicher Objekte bietet. Strategien neigen dazu, hier zu fallen. Staatliche Objekte fallen hierher. Ich halte es nicht für zu unvernünftig, ein Interface so zu gestalten, dass es unveränderlich erscheint, und es als solches in Ihrer API zu dokumentieren.
Das heißt, Menschen neigen dazu, Plain Old Data-Objekte (im Folgenden POD-Objekte) und sogar einfache (oft unveränderliche) Strukturen zu überschneiden. Wenn Ihr Code keine vernünftigen Alternativen zu einer grundlegenden Struktur hat, benötigt er keine Schnittstelle. Nein, Komponententests sind kein ausreichender Grund, Ihr Design zu ändern (verspotteter Datenbankzugriff ist nicht der Grund, warum Sie eine Schnittstelle dazu bereitstellen, sondern Flexibilität für zukünftige Änderungen) - es ist nicht das Ende der Welt, wenn Ihre Tests durchgeführt werden Verwenden Sie diese grundlegende Grundstruktur wie sie ist.
quelle
Ich denke nicht, dass der Accessor-Implementierer sich Sorgen machen muss. Wenn
interface X
unveränderlich sein soll, liegt es dann nicht in der Verantwortung des Schnittstellenimplementierers, sicherzustellen, dass die Schnittstelle unveränderlich implementiert wird?Meiner Meinung nach gibt es jedoch keine unveränderliche Schnittstelle - der von einer Schnittstelle angegebene Codevertrag gilt nur für die offengelegten Methoden eines Objekts und nichts über die Interna eines Objekts.
Es ist weitaus üblicher, Unveränderlichkeit als Dekorateur anstatt als Schnittstelle zu sehen, aber die Machbarkeit dieser Lösung hängt wirklich von der Struktur Ihres Objekts und der Komplexität Ihrer Implementierung ab.
quelle
MutableObject
hatn
Methoden , die ihren Zustand ändern undm
Methoden , den Rückkehrzustand, auchImmutableDecorator
weiterhin die Methoden , den Rückkehrzustand (auszusetzenm
) und in Abhängigkeit von der Umgebung, assert oder Eine Ausnahme auslösen, wenn eine der veränderlichen Methoden aufgerufen wird.In C # sollte eine Methode, die ein Objekt eines "unveränderlichen" Typs erwartet, keinen Parameter eines Schnittstellentyps haben, da C # -Schnittstellen den Unveränderlichkeitsvertrag nicht angeben können. Daher ist die Richtlinie, die Sie selbst vorschlagen, in C # bedeutungslos, da Sie dies nicht in erster Linie tun können (und zukünftige Versionen der Sprachen dies wahrscheinlich nicht ermöglichen werden).
Ihre Frage ergibt sich aus einem subtilen Missverständnis der Artikel von Eric Lippert über Unveränderlichkeit. Eric hat die Schnittstellen nicht definiert
IStack<T>
undIQueue<T>
Verträge für unveränderliche Stapel und Warteschlangen festgelegt. Sie tun es nicht. Er definierte sie der Einfachheit halber. Diese Schnittstellen ermöglichten es ihm, verschiedene Typen für leere Stapel und Warteschlangen zu definieren. Wir können ein anderes Design und eine andere Implementierung eines unveränderlichen Stapels unter Verwendung eines einzelnen Typs vorschlagen, ohne dass eine Schnittstelle oder ein separater Typ zur Darstellung des leeren Stapels erforderlich ist. Der resultierende Code sieht jedoch nicht so sauber aus und wäre etwas weniger effizient.Bleiben wir nun bei Eric's Design. Eine Methode, die einen unveränderlichen Stapel benötigt, muss einen Parameter vom Typ haben
Stack<T>
und nicht die allgemeine Schnittstelle,IStack<T>
die den abstrakten Datentyp eines Stapels im allgemeinen Sinne darstellt. Es ist nicht offensichtlich, wie dies bei Verwendung von Erics unveränderlichem Stapel zu tun ist, und er hat dies in seinen Artikeln nicht erörtert, aber es ist möglich. Das Problem liegt in der Art des leeren Stapels. Sie können dieses Problem lösen, indem Sie sicherstellen, dass Sie niemals einen leeren Stapel erhalten. Dies kann sichergestellt werden, indem ein Dummy-Wert als erster Wert auf dem Stapel abgelegt wird und niemals abgelegt wird. Auf diese Weise können Sie die Ergebnisse vonPush
undPop
bis sicher umsetzenStack<T>
.Mit
Stack<T>
ArbeitsgeräteIStack<T>
kann nützlich sein. Sie können Methoden definieren, die einen Stapel erfordern, einen beliebigen Stapel, nicht unbedingt einen unveränderlichen Stapel. Diese Methoden können einen Parameter vom Typ habenIStack<T>
. Auf diese Weise können Sie unveränderliche Stapel übergeben. IdealerweiseIStack<T>
wäre dies Teil der Standardbibliothek. In .NET gibt es keineIStack<T>
, aber es gibt andere Standardschnittstellen, die der unveränderliche Stapel implementieren kann, wodurch der Typ nützlicher wird.Der alternative Entwurf und die Implementierung des unveränderlichen Stapels, auf den ich früher Bezug genommen habe, verwenden eine Schnittstelle, die aufgerufen wird
IImmutableStack<T>
. Natürlich macht das Einfügen von "Unveränderlich" in den Schnittstellennamen nicht jeden Typ, der ihn implementiert, unveränderlich. In dieser Schnittstelle ist der Vertrag über die Unveränderlichkeit jedoch nur mündlich. Ein guter Entwickler sollte es respektieren.Wenn Sie eine kleine interne Bibliothek entwickeln, können Sie sich mit allen Mitgliedern des Teams auf die Einhaltung dieses Vertrags einigen und diese
IImmutableStack<T>
als Parameter verwenden. Andernfalls sollten Sie den Schnittstellentyp nicht verwenden.Da Sie die Frage C # markiert haben, möchte ich hinzufügen, dass es in der C # -Spezifikation keine DTOs und PODs gibt. Daher wird die Frage verbessert, wenn sie verworfen oder genau definiert werden.
quelle