Legitime "echte Arbeit" in einem Konstruktor?


23

Ich arbeite an einem Design, stoße aber immer wieder auf eine Straßensperre. Ich habe eine bestimmte Klasse (ModelDef), die im Wesentlichen der Eigentümer eines komplexen Knotenbaums ist, der durch Analysieren eines XML-Schemas (Think DOM) erstellt wurde. Ich möchte guten Konstruktionsprinzipien (SOLID) folgen und sicherstellen, dass das resultierende System leicht testbar ist. Ich habe die Absicht, DI zu verwenden, um Abhängigkeiten an den Konstruktor von ModelDef zu übergeben (damit diese bei Bedarf während des Testens problemlos ausgetauscht werden können).

Womit ich jedoch zu kämpfen habe, ist die Erstellung des Knotenbaums. Dieser Baum wird ganz aus einfachen "Wert" -Objekten bestehen, die nicht unabhängig getestet werden müssen. (Möglicherweise übergebe ich trotzdem eine abstrakte Factory an ModelDef, um die Erstellung dieser Objekte zu unterstützen.)

Aber ich lese immer wieder, dass ein Konstruktor keine echte Arbeit leisten sollte (zB Fehler: Konstruktor leistet echte Arbeit ). Das ist für mich sehr sinnvoll, wenn "echte Arbeit" bedeutet, schwergewichtige Objekte zu konstruieren, die man möglicherweise später zum Testen herausnehmen möchte. (Diese sollten über DI übergeben werden.)

Aber was ist mit leichtgewichtigen Objekten wie diesem Knotenbaum? Der Baum muss irgendwo angelegt werden, oder? Warum nicht über den Konstruktor von ModelDef (z. B. mit einer buildNodeTree () -Methode)?

Ich möchte den Knotenbaum nicht wirklich außerhalb von ModelDef erstellen und dann (über Konstruktor DI) übergeben, da das Erstellen des Knotenbaums durch Analysieren des Schemas eine erhebliche Menge an komplexem Code erfordert - Code, der gründlich getestet werden muss . Ich möchte es nicht zum "Kleben" von Code degradieren (was relativ trivial sein sollte und wahrscheinlich nicht direkt getestet wird).

Ich habe darüber nachgedacht, den Code zum Erstellen des Knotenbaums in ein separates "Builder" -Objekt einzufügen, zögere jedoch, ihn als "Builder" zu bezeichnen, da er nicht wirklich mit dem Builder-Muster übereinstimmt (das anscheinend mehr mit der Beseitigung des Teleskopierens zu tun hat) Konstrukteure). Aber selbst wenn ich es etwas anderes nannte (z. B. NodeTreeConstructor), fühlt es sich immer noch wie ein Hack an, nur um zu vermeiden, dass der ModelDef-Konstruktor den Knotenbaum erstellt. Es muss irgendwo gebaut werden; Warum nicht in dem Objekt, das es besitzen wird?


7
Sie sollten jedoch immer vorsichtig mit solchen pauschalen Aussagen sein. Die allgemeine Faustregel lautet: Code ist klar, funktionell, einfach zu testen, wiederzuverwenden und zu warten, unabhängig von der Art und Weise, die von Ihrer Situation abhängt. Wenn Sie nur Code-Komplexität und Verwirrung erlangen, wenn Sie versuchen, einer solchen "Regel" zu folgen, war dies keine angemessene Regel für Ihre Situation. Alle diese "Muster" und Sprachmerkmale sind Werkzeuge; Verwenden Sie die beste für Ihren speziellen Job.
Jason C

Antworten:


26

Und außer dem, was Ross Patterson vorschlug, betrachten Sie diese Position, die genau das Gegenteil ist:

  1. Nehmen Sie Maximen wie "Du sollst in deinen Konstrukteuren keine wirkliche Arbeit leisten" mit einem Körnchen Salz.

  2. Ein Konstruktor ist eigentlich nichts anderes als eine statische Methode. Strukturell gibt es also keinen großen Unterschied zwischen:

    a) einen einfachen Konstruktor und eine Reihe komplexer statischer Factory-Methoden und

    b) einen einfachen Konstruktor und eine Reihe komplexerer Konstruktoren.

Ein erheblicher Teil der negativen Einstellung, echte Konstrukteure zu beschäftigen, stammt aus einer bestimmten Periode in der Geschichte von C ++, in der diskutiert wurde, in welchem ​​Zustand das Objekt verbleibt, wenn innerhalb des Konstruktors eine Ausnahme ausgelöst wird, und ob In einem solchen Fall sollte der Destruktor aufgerufen werden. Dieser Teil der Geschichte von C ++ ist vorbei und das Problem wurde gelöst, während es in Sprachen wie Java noch nie ein solches Problem gab.

Meiner Meinung newnach sollten Sie in Ordnung sein , wenn Sie die Verwendung im Konstruktor einfach vermeiden (wie Ihre Absicht, Dependency Injection einzusetzen, andeutet). Ich lache über Aussagen wie "Bedingte oder Schleifenlogik in einem Konstruktor ist ein Warnsignal für einen Fehler".

Abgesehen davon würde ich persönlich die XML-Parsing-Logik aus dem Konstruktor herausnehmen, nicht weil es böse ist, komplexe Logik in einem Konstruktor zu haben, sondern weil es gut ist, dem Prinzip der "Trennung von Bedenken" zu folgen. Daher würde ich die XML-Parsing-Logik in eine separate Klasse verschieben, nicht in statische Methoden, die zu Ihrer ModelDefKlasse gehören.

Änderung

Ich nehme an, wenn Sie eine Methode haben, außerhalb ModelDefderer eine ModelDeffrom XML erstellt wird, müssen Sie eine dynamische temporäre Baumdatenstruktur instanziieren, diese durch Parsen Ihrer XML auffüllen und dann Ihre neue ModelDefÜbergabe dieser Struktur als Konstruktorparameter erstellen . Das könnte man sich vielleicht als Anwendung des "Builder" -Musters vorstellen. Es gibt eine sehr enge Analogie zwischen dem, was Sie tun möchten, und dem String& StringBuilderPaar. Ich habe jedoch diese Frage und Antwort gefunden , die aus mir unklaren Gründen nicht zu stimmen scheint: Stackoverflow - StringBuilder und Builder Pattern . Um hier eine lange Debatte darüber zu vermeiden, ob das StringBuilder"Builder" -Muster umgesetzt wird oder nicht, würde ich sagen, lassen Sie sich gerne davon inspirieren, wieStrungBuilder arbeitet daran, eine Lösung zu finden, die Ihren Anforderungen entspricht, und es aufzuschieben, eine Anwendung des "Builder" -Musters zu verwenden, bis dieses kleine Detail geklärt ist.

Siehe diese brandneue Frage: Programmierer SE: Ist "StringBuilder" eine Anwendung des Builder-Entwurfsmusters?


3
@RichardLevasseur Ich erinnere mich, dass es Anfang bis Mitte der neunziger Jahre unter C ++ - Programmierern Anlass zur Sorge und Debatte gab. Wenn Sie sich diesen Beitrag ansehen : gotw.ca/gotw/066.htm, werden Sie feststellen , dass er ziemlich kompliziert ist und dass ziemlich komplizierte Dinge eher umstritten sind. Ich weiß es nicht genau, aber ich glaube, dass in den frühen Neunzigern ein Teil davon noch nicht einmal standardisiert war. Leider kann ich keine gute Referenz liefern.
Mike Nakis

1
@Gurtz Ich würde mir eine solche Klasse als anwendungsspezifische "xml-Dienstprogramme" vorstellen, da das Format der xml-Datei (oder die Struktur des Dokuments) wahrscheinlich an die jeweilige Anwendung gebunden ist, die Sie entwickeln, unabhängig von irgendwelchen Möglichkeiten Verwenden Sie Ihr "ModelDef" erneut.
Mike Nakis

1
@Gurtz also, ich würde sie wahrscheinlich zu Instanzmethoden der Hauptklasse "Application" machen, oder, wenn das zu umständlich ist, zu statischen Methoden einer Hilfsklasse, auf eine Weise, die der von Ross Patterson vorgeschlagenen sehr ähnlich ist.
Mike Nakis

1
@Gurtz entschuldigt sich dafür, dass er den "Builder" -Ansatz früher nicht speziell angesprochen hat. Ich habe meine Antwort geändert.
Mike Nakis

3
@Gurtz Es ist möglich, aber außerhalb der akademischen Neugier spielt es keine Rolle. Lass dich nicht auf das "Muster gegen Muster" ein. Muster sind eigentlich nur Namen, mit denen andere Benutzer auf bequeme Weise allgemeine / nützliche Codiertechniken beschreiben können. Tun Sie, was Sie tun müssen, und schlagen Sie später ein Etikett darauf, wenn Sie es beschreiben müssen. Es ist vollkommen in Ordnung, etwas zu implementieren, das "in gewisser Weise wie das Builder-Muster" ist, solange Ihr Code Sinn ergibt. Es ist vernünftig, sich beim Erlernen neuer Techniken auf Muster zu konzentrieren, aber denken Sie nicht, dass alles, was Sie tun, ein benanntes Muster sein muss.
Jason C

9

Sie geben bereits die besten Gründe an, diese Arbeit nicht im ModelDefKonstruktor auszuführen:

  1. Das Parsen eines XML-Dokuments in einen Knotenbaum ist nichts "Leichtes".
  2. Es gibt nichts Offensichtliches an einem ModelDef, das besagt, dass es nur aus einem XML-Dokument erstellt werden kann.

Es klingt wie Ihre Klasse eine Vielzahl von statischen Methoden wie haben sollte ModelDef.FromXmlString(string xmlDocument), ModelDef.FromXmlDoc(XmlDoc parsedNodeTree), usw.


Danke für die Antwort! Zum Vorschlag statischer Methoden. Wären dies statische Fabriken, die eine Instanz von ModelDef (aus den verschiedenen XML-Quellen) erstellen? Oder sind sie dafür verantwortlich, ein bereits erstelltes ModelDef-Objekt zu laden? In letzterem Fall würde ich befürchten, dass das Objekt nur teilweise initialisiert wird (da ein ModelDef einen vollständig zu initialisierenden Knotenbaum benötigt). Gedanken?
Gurtz

3
Entschuldigen Sie, dass ich eingreifen möchte, aber was Ross meint, sind statische Factory-Methoden, die vollständig konstruierte Instanzen zurückgeben. Der vollständige Prototyp wäre so etwas wie public static ModelDef createFromXmlString( string xmlDocument ). Dies ist eine weit verbreitete Praxis. Manchmal mache ich es auch. Mein Vorschlag, dass Sie auch nur Konstruktoren verwenden können, ist eine meiner Standardantworten in Situationen, in denen ich vermute, dass alternative Ansätze ohne guten Grund als "nicht koscher" abgetan werden.
Mike Nakis

1
@ Mike-Nakis, danke für die Klarstellung. In diesem Fall würde die statische Factory-Methode den Knotenbaum erstellen und diesen dann an den Konstruktor (möglicherweise privat) von ModelDef übergeben. Sinn ergeben. Vielen Dank.
Gurtz

Genau.
Ross Patterson

5

Ich habe diese "Regel" schon einmal gehört. Nach meiner Erfahrung ist es sowohl wahr als auch falsch.

In der eher "klassischen" Objektorientierung sprechen wir über Objekte, die Zustand und Verhalten einschließen. Daher sollte ein Objektkonstruktor sicherstellen, dass das Objekt in einen gültigen Zustand initialisiert wird (und einen Fehler signalisieren, wenn die angegebenen Argumente das Objekt nicht gültig machen). Wenn Sie sicherstellen, dass ein Objekt in einem gültigen Zustand initialisiert ist, klingt das für mich nach echter Arbeit. Und diese Idee hat Vorteile, wenn Sie ein Objekt haben, das nur die Initialisierung in einen gültigen Zustand über den Konstruktor zulässt und das Objekt den Zustand ordnungsgemäß einkapselt, sodass jede Methode, die den Zustand ändert, auch überprüft, ob der Zustand nicht in etwas Schlechtes geändert wird ... dann garantiert dieses Objekt im Wesentlichen, dass es "immer gültig" ist. Das ist eine wirklich schöne Immobilie!

Das Problem tritt also im Allgemeinen auf, wenn wir versuchen, alles in kleine Stücke zu zerlegen, um Dinge zu testen und zu verspotten. Denn wenn ein Objekt wirklich richtig gekapselt ist, können Sie nicht wirklich hineinkommen und den FooBarService durch Ihren verspotteten FooBarService ersetzen, und Sie können (wahrscheinlich) die Werte nicht einfach so ändern, wie es Ihren Tests entspricht (oder es kann eine Weile dauern) viel mehr Code als eine einfache Zuweisung).

So bekommen wir die andere "Schule des Denkens", die SOLID ist. Und in dieser Denkschule ist es höchstwahrscheinlich viel wahrer, dass wir im Konstruktor keine wirkliche Arbeit leisten sollten. SOLID-Code ist oft (aber nicht immer) einfacher zu testen. Kann aber auch schwerer zu überlegen sein. Wir zerlegen unseren Code in kleine Objekte mit einer einzigen Verantwortung, und daher verkapseln die meisten unserer Objekte ihren Zustand nicht mehr (und enthalten im Allgemeinen entweder Zustand oder Verhalten). Der Validierungscode wird im Allgemeinen in eine Validierungsklasse extrahiert und vom Status getrennt gehalten. Aber jetzt, da wir den Zusammenhalt verloren haben, können wir nicht mehr sicher sein, dass unsere Objekte gültig sind, wenn wir sie erhalten und vollständig sindNatürlich müssen wir immer überprüfen, ob die Voraussetzungen, die wir für das Objekt halten, erfüllt sind, bevor wir versuchen, etwas mit dem Objekt zu tun. (Natürlich führen Sie im Allgemeinen die Validierung in einer Ebene durch und nehmen dann an, dass das Objekt in niedrigeren Ebenen gültig ist.) Aber es ist einfacher zu testen!

Also, wer hat Recht?

Niemand wirklich. Beide Denkrichtungen haben ihre Vorzüge. Derzeit ist SOLID der letzte Schrei und alle reden über SRP und Open / Closed und all diesen Jazz. Nur weil etwas beliebt ist, ist es noch lange nicht die richtige Wahl für jede einzelne Anwendung. Es kommt also darauf an. Wenn Sie in einer Codebasis arbeiten, die stark den SOLID-Prinzipien folgt, ist echte Arbeit im Konstruktor wahrscheinlich eine schlechte Idee. Aber ansonsten schauen Sie sich die Situation an und versuchen Sie, Ihr Urteilsvermögen zu nutzen. Welche Eigenschaften hat Ihr Objekt Gewinn von der Arbeit in dem Konstruktor zu tun, was Eigenschaften tut es verlieren ? Wie gut passt es zur Gesamtarchitektur Ihrer Anwendung?

Echte Arbeit im Konstruktor ist kein Gegenmuster, sondern kann genau das Gegenteil bewirken, wenn sie an den richtigen Stellen verwendet wird. Aber es sollte klar dokumentiert sein (zusammen mit den Ausnahmen, falls vorhanden) und wie bei jeder Designentscheidung sollte es in den allgemeinen Stil passen, der in der aktuellen Codebasis verwendet wird.


Das ist eine fantastische Antwort.
Jrahhali

0

Es gibt ein grundlegendes Problem mit dieser Regel und es ist das, was "echte Arbeit" ausmacht.

Sie können dem Originalartikel entnehmen, der in der Frage veröffentlicht wurde, dass der Autor versucht zu definieren, was "echte Arbeit" ist, aber es sind schwerwiegende Fehler aufgetreten. Damit eine Praxis gut ist, muss sie ein genau definiertes Prinzip sein. Damit meine ich, dass die Idee in Bezug auf Softwareentwicklung portabel (unabhängig von jeder Sprache), getestet und bewährt sein sollte. Das meiste, was in diesem Artikel argumentiert wird, passt nicht in dieses erste Kriterium. Hier sind einige Indikatoren, die der Autor in diesem Artikel erwähnt, was "echte Arbeit" ist und warum sie keine schlechten Definitionen sind.

Verwendung des newSchlüsselworts . Diese Definition ist grundlegend fehlerhaft, weil sie domänenspezifisch ist. Einige Sprachen verwenden das newSchlüsselwort nicht. Letztendlich deutet er an, dass es nicht darum gehen sollte, andere Objekte zu konstruieren. In vielen Sprachen sind jedoch selbst die grundlegendsten Werte Objekte. Jeder im Konstruktor zugewiesene Wert erstellt also auch ein neues Objekt. Das macht diese Idee auf bestimmte Sprachen beschränkt und ein schlechter Indikator dafür, was "echte Arbeit" ausmacht.

Objekt nach Abschluss des Konstruktors nicht vollständig initialisiert . Dies ist eine gute Regel, widerspricht aber auch einigen anderen in diesem Artikel genannten Regeln. Ein gutes Beispiel dafür, wie das den anderen widersprechen könnte, ist die Frage , die mich hierher gebracht hat. In dieser Frage ist jemand besorgt, die sortMethode in einem Konstruktor zu verwenden, der aufgrund dieses Prinzips JavaScript zu sein scheint. In diesem Beispiel hat die Person ein Objekt erstellt, das eine sortierte Liste anderer Objekte darstellt. Stellen Sie sich zum Zweck der Diskussion vor, wir hätten eine unsortierte Liste von Objekten und wir bräuchten ein neues Objekt, um eine sortierte Liste darzustellen. Wir benötigen dieses neue Objekt, da ein Teil unserer Software eine sortierte Liste erwartet und wir dieses Objekt aufrufen könnenSortedList. Dieses neue Objekt akzeptiert eine unsortierte Liste und das resultierende Objekt sollte eine jetzt sortierte Liste von Objekten darstellen. Wenn wir die anderen in diesem Dokument erwähnten Regeln befolgen würden, nämlich keine statischen Methodenaufrufe, keine Kontrollflussstrukturen, nur Zuweisung, dann würde das resultierende Objekt nicht in einem gültigen Zustand konstruiert, der die andere Regel der vollständigen Initialisierung verletzt nachdem der Konstruktor fertig ist. Um dies zu beheben, müssten wir einige grundlegende Arbeiten ausführen, um die unsortierte Liste im Konstruktor zu sortieren. Dies würde die 3 anderen Regeln brechen und die anderen Regeln irrelevant machen.

Letztendlich ist diese Regel, in einem Konstruktor keine "echte Arbeit" zu leisten, schlecht definiert und fehlerhaft. Der Versuch zu definieren, was "echte Arbeit" ist, ist eine Übung in der Sinnlosigkeit. Die beste Regel in diesem Artikel ist, dass ein Konstruktor nach Abschluss vollständig initialisiert werden sollte. Es gibt eine Vielzahl weiterer bewährter Methoden, die die Arbeit in einem Konstruktor einschränken. Die meisten davon lassen sich in SOLID-Prinzipien zusammenfassen, und dieselben Prinzipien würden Sie nicht daran hindern, im Konstruktor zu arbeiten.

PS. Ich fühle mich verpflichtet zu sagen, dass, während ich hier behaupte, dass es nichts Falsches ist, im Konstruktor etwas zu tun, es auch nicht der Ort ist, an dem ein Haufen Arbeit getan werden kann. SRP schlägt vor, dass ein Konstruktor gerade genug Arbeit leistet, um ihn gültig zu machen. Wenn Ihr Konstruktor über zu viele Codezeilen verfügt (hochgradig subjektiv, wie ich weiß), verstößt er wahrscheinlich gegen dieses Prinzip und könnte möglicherweise in kleinere, besser definierte Methoden und Objekte unterteilt werden.

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.