Deklarieren Sie keine Schnittstellen für unveränderliche Objekte

27

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.

Matthew Watson
quelle
3
+1 tolle Frage. Erklären Sie bitte, warum es für Sie so wichtig ist, dass das Objekt zu dieser bestimmten unveränderlichen Klasse gehört und nicht nur dasselbe Interface implementiert. Es ist möglich, dass Ihre Probleme anderswo verwurzelt sind. Die Abhängigkeitsinjektion ist für Komponententests äußerst wichtig, hat jedoch viele andere wichtige Vorteile, die sich alle aufheben, wenn Sie Ihren Code zwingen, eine bestimmte unveränderliche Klasse zu erfordern.
Steven Doggart
1
Ich bin etwas verwirrt über Ihre Verwendung des Begriffs unveränderlich . Um es klar auszudrücken, meinen Sie eine versiegelte Klasse, die nicht vererbt und überschrieben werden kann, oder meinen Sie, dass die bereitgestellten Daten schreibgeschützt sind und nach dem Erstellen des Objekts nicht mehr geändert werden können. Mit anderen Worten, möchten Sie sicherstellen, dass das Objekt von einem bestimmten Typ ist oder dass sich sein Wert niemals ändert. In meinem ersten Kommentar habe ich angenommen, dass ersteres gemeint ist, aber jetzt mit Ihrer Bearbeitung klingt es eher wie letzteres. Oder geht es Ihnen um beides?
Steven Doggart
3
Ich bin verwirrt, warum nicht einfach Setter von der Schnittstelle lassen? Andererseits werde ich müde, Schnittstellen für Objekte zu sehen, bei denen es sich wirklich um Domänenobjekte und / oder DTOs handelt, die Fabriken für diese Objekte erfordern. Dies führt bei mir zu kognitiven Dissonanzen.
Michael Brown
2
Was ist mit Schnittstellen für Klassen, die alle unveränderlich sind? Zum Beispiel die Java - Nummer ermöglicht Klasse eine eine definieren , List<Number>die halten kann Integer, Float, Long, BigDecimal, etc ... All das sich unveränderlich sind.
1
@ MikeBrown Ich denke, nur weil die Schnittstelle Unveränderlichkeit impliziert, bedeutet dies nicht, dass die Implementierung dies erzwingt. Sie könnten es einfach der Konvention überlassen (dh die Schnittstelle dokumentieren, dass Unveränderlichkeit eine Voraussetzung ist), aber Sie könnten mit einigen wirklich schlimmen Problemen enden, wenn jemand gegen die Konvention verstößt.
Vaughandroid

Antworten:

17

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.

Steven Doggart
quelle
Code, der speziell mit einem DTO-Objekt oder einem Objekt mit unveränderlichem Wert umgeht, sollte die Merkmale dieses Typs und nicht die einer Schnittstelle verwenden. Dies bedeutet jedoch nicht, dass es keine Anwendungsfälle für eine Schnittstelle geben würde, die von einer solchen Klasse und auch implementiert wird von anderen Klassen mit Methoden, die versprechen, Werte zurückzugeben, die für einen bestimmten Zeitraum gültig sind (z. B. wenn die Schnittstelle an eine Funktion übergeben wird, sollten die Methoden gültige Werte zurückgeben, bis diese Funktion zurückgibt). Eine solche Schnittstelle kann hilfreich sein, wenn es eine Reihe von Typen gibt, die ähnliche Daten kapseln und man möchte ...
Supercat
... um Daten von einem zum anderen kopieren zu können. Wenn alle Typen, die bestimmte Daten enthalten, dieselbe Schnittstelle zum Lesen implementieren, können diese Daten problemlos zwischen allen Typen kopiert werden, ohne dass jeder wissen muss, wie er von jedem der anderen Daten importiert.
Supercat
An diesem Punkt können Sie auch Ihre Getter (und Setter, wenn Ihre DTOs aus irgendeinem dummen Grund veränderbar sind) eliminieren und die Felder öffentlich machen. Sie werden dort nie eine Logik aufstellen, oder?
Kevin
@ Kevin Ich nehme an, aber mit dem modernen Komfort von Auto-Eigenschaften ist es so einfach, sie Eigenschaften zu machen, warum nicht? Ich nehme an, wenn Leistung und Effizienz von größter Bedeutung sind, dann ist das vielleicht wichtig. Auf jeden Fall stellt sich die Frage, welche Ebene der "Logik" Sie in einem DTO zulassen. Selbst wenn Sie eine Validierungslogik in ihre Eigenschaften einfügen, wäre es kein Problem, sie zu testen, solange es keine Abhängigkeiten gibt, die eine Verspottung erfordern würden. Solange der DTO keine abhängigen Geschäftsobjekte verwendet, ist es sicher, eine kleine Logik in sie zu integrieren, da sie weiterhin testbar sind.
Steven Doggart
Ich persönlich bevorzuge es, sie so sauber wie möglich zu halten, aber es gibt immer Zeiten, in denen es notwendig ist, die Regeln zu biegen, und deshalb ist es besser, die Tür für alle Fälle offen zu lassen.
Steven Doggart
7

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.

Travis Parks
quelle
Normalerweise speichere ich Schnittstellen für den Fall, dass ich eine Abhängigkeitsinjektion für Unit-Tests benötige.
Travis Parks
5

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.

Telastyn
quelle
2

Code wird in vielen Fällen viel einfacher, wenn Sie wissen, dass etwas unveränderlich ist - was Sie nicht tun, wenn Sie eine Schnittstelle erhalten haben.

Ich denke nicht, dass der Accessor-Implementierer sich Sorgen machen muss. Wenn interface Xunverä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.

Jonathan Rich
quelle
1
Können Sie ein Beispiel für die Implementierung von Unveränderlichkeit als Dekorateur nennen?
Vaughandroid
Nicht trivial, ich glaube nicht, aber es ist eine relativ einfache Gedanke Übung - wenn MutableObjecthat nMethoden , die ihren Zustand ändern und mMethoden , den Rückkehrzustand, auch ImmutableDecoratorweiterhin die Methoden , den Rückkehrzustand (auszusetzen m) und in Abhängigkeit von der Umgebung, assert oder Eine Ausnahme auslösen, wenn eine der veränderlichen Methoden aufgerufen wird.
Jonathan Rich
Ich mag es wirklich nicht, die Sicherheit der Kompilierungszeit in die Möglichkeit von Laufzeitausnahmen umzuwandeln ...
Matthew Watson
OK, aber wie kann ImmutableDecorator wissen, ob eine bestimmte Methode den Status ändert oder nicht?
Vaughandroid
Sie können in keinem Fall sicher sein, ob eine Klasse, die Ihre Schnittstelle implementiert, in Bezug auf die von Ihrer Schnittstelle definierten Methoden unveränderlich ist. @Baqueta Der Dekorateur muss Kenntnisse über die Implementierung der Basisklasse haben.
Jonathan Rich
0

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.

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>und IQueue<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 von Pushund Popbis sicher umsetzen Stack<T>.

Mit Stack<T>Arbeitsgeräte IStack<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 haben IStack<T>. Auf diese Weise können Sie unveränderliche Stapel übergeben. Idealerweise IStack<T>wäre dies Teil der Standardbibliothek. In .NET gibt es keine IStack<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.

Hadi Brais
quelle