wie komplex ein Konstruktor sein sollte

18

Ich diskutiere mit meinem Kollegen darüber, wie viel Arbeit ein Konstrukteur leisten kann. Ich habe eine Klasse B, die intern ein anderes Objekt A erfordert. Objekt A ist eines von wenigen Mitgliedern, die Klasse B für ihre Arbeit benötigt. Alle öffentlichen Methoden hängen vom internen Objekt A ab. Informationen zu Objekt A werden in der Datenbank gespeichert. Daher versuche ich, sie zu überprüfen und abzurufen, indem ich sie in der Datenbank im Konstruktor nachschaue. Mein Mitarbeiter wies darauf hin, dass der Konstruktor nur die Konstruktorparameter erfassen sollte. Da alle öffentlichen Methoden ohnehin fehlschlagen würden, wenn Objekt A nicht mit den Eingaben für den Konstruktor gefunden wird, habe ich argumentiert, dass es besser ist, eine Instanz früh im Konstruktor auszulösen, anstatt zuzulassen, dass sie erstellt und später fehlschlägt.

Was denken andere? Ich verwende C #, wenn das einen Unterschied macht.

Lesen Gibt es jemals einen Grund, die gesamte Arbeit eines Objekts in einem Konstruktor zu erledigen? Ich frage mich, ob das Abrufen von Objekt A durch Aufrufen von DB Teil der "sonstigen Initialisierung, die erforderlich ist, um das Objekt einsatzbereit zu machen" ist. Wenn der Benutzer falsche Werte an den Konstruktor übergibt, kann ich keine seiner öffentlichen Methoden verwenden.

Konstruktoren sollten die Felder eines Objekts instanziieren und alle anderen Initialisierungen vornehmen, die erforderlich sind, um das Objekt einsatzbereit zu machen. Dies bedeutet im Allgemeinen, dass Konstruktoren klein sind, aber es gibt Szenarien, in denen dies einen erheblichen Arbeitsaufwand bedeuten würde.

Taein Kim
quelle
8
Ich stimme dir nicht zu. Wenn Sie sich das Prinzip der Abhängigkeitsinversion ansehen, ist es besser, wenn Objekt A in seinem Konstruktor in einem gültigen Zustand an Objekt B übergeben wird.
Jimmy Hoffa
5
Sind Sie fragen, wie viel kann getan werden? Wie viel sollte getan werden? Oder wie viel von Ihrer spezifischen Situation sollte getan werden? Das sind drei sehr unterschiedliche Fragen.
Bobson
1
Ich stimme Jimmy Hoffas Vorschlag zu, die Abhängigkeitsinjektion (oder "Abhängigkeitsinversion") zu untersuchen. Das war mein erster Gedanke beim Lesen der Beschreibung, denn es hört sich so an, als wärst du schon auf halbem Weg; Sie haben bereits Funktionen in Klasse A unterteilt, die Klasse B verwendet. Ich kann nicht mit Ihrem Fall sprechen, aber ich finde im Allgemeinen, dass das Umgestalten von Code zur Verwendung des Abhängigkeitsinjektionsmusters Klassen einfacher / weniger miteinander verflochten macht.
Dr. Wily's Apprentice
1
Bobson, ich habe den Titel in "sollte" geändert. Ich bitte hier um Best Practice.
Taein Kim
1
@ JimmyHoffa: Vielleicht solltest du deinen Kommentar zu einer Antwort erweitern.
Kevin Cline

Antworten:

22

Ihre Frage besteht aus zwei völlig getrennten Teilen:

Sollte ich eine Ausnahme vom Konstruktor auslösen oder sollte die Methode fehlschlagen?

Dies ist eindeutig eine Anwendung des Fail-Fast- Prinzips. Das Fehlschlagen des Konstruktors ist viel einfacher zu debuggen, als herauszufinden, warum die Methode fehlschlägt. Beispielsweise kann es sein, dass Sie die Instanz bereits aus einem anderen Teil des Codes erstellt haben und beim Aufrufen von Methoden Fehler auftreten. Ist es offensichtlich, dass das Objekt falsch erstellt wurde? Nein.

Wie für das Problem "wickeln Sie den Anruf in try / catch". Ausnahmen sollen außergewöhnlich sein . Wenn Sie wissen, dass ein Code eine Ausnahme auslöst, schließen Sie den Code nicht in try / catch ein, sondern überprüfen die Parameter dieses Codes, bevor Sie den Code ausführen, der eine Ausnahme auslösen kann. Ausnahmen sollen nur sicherstellen, dass das System nicht in einen ungültigen Zustand gerät. Wenn Sie wissen, dass einige Eingabeparameter zu einem ungültigen Status führen können, stellen Sie sicher, dass diese Parameter niemals auftreten. Auf diese Weise müssen Sie nur an Stellen try / catch ausführen, an denen Sie die Ausnahme, die sich normalerweise an der Systemgrenze befindet, logisch behandeln können.

Kann ich vom Konstruktor aus auf "andere Teile des Systems wie DB" zugreifen?

Ich denke, das widerspricht dem Grundsatz des geringsten Erstaunens . Nicht viele Leute würden erwarten, dass der Konstruktor auf eine Datenbank zugreift. Also nein, das solltest du nicht tun.

Euphorisch
quelle
2
Die geeignetere Lösung besteht normalerweise darin, eine Methode vom Typ "open" hinzuzufügen, bei der die Konstruktion kostenlos ist, die jedoch erst verwendet werden kann, wenn "open" / "connect" / etc ausgeführt wird. Hier findet die gesamte eigentliche Initialisierung statt. Fehlgeschlagene Konstruktoren können zu allen möglichen Problemen führen, wenn Sie in Zukunft Objekte teilweise konstruieren möchten. Dies ist eine nützliche Funktion.
Jimmy Hoffa
@ JimmyHoffa Ich sehe keinen Unterschied zwischen Konstruktormethode und spezifischer Konstruktionsmethode.
Euphorischer
4
Sie können nicht, aber viele tun. Überlegen Sie, ob der Konstruktor beim Erstellen eines Verbindungsobjekts eine Verbindung herstellt. Ist das Objekt verwendbar, wenn es nicht verbunden ist? In beiden Fragen lautet die Antwort Nein, da es hilfreich ist, Verbindungsobjekte teilweise zu konstruieren, damit Sie Einstellungen und Dinge anpassen können, bevor Sie die Verbindung öffnen. Dies ist ein gängiger Ansatz für dieses Szenario. Ich denke immer noch, dass die Konstruktorinjektion der richtige Ansatz für Szenarien ist, in denen Objekt A eine Ressourcenabhängigkeit aufweist, aber eine offene Methode immer noch besser ist, als wenn der Konstruktor die gefährliche Arbeit erledigt.
Jimmy Hoffa
@JimmyHoffa Aber das ist eine spezielle Anforderung der Verbindungsklasse, nicht die allgemeine Regel des OOP-Designs. Ich würde es eigentlich einen Ausnahmefall nennen, als eine Regel. Sie sprechen im Grunde über Builder-Muster.
Euphorischer
2
Es ist keine Anforderung, es war eine Designentscheidung. Sie hätten den Konstruktor veranlassen können, eine Verbindungszeichenfolge zu verwenden und die Verbindung sofort nach der Erstellung herzustellen. Sie haben sich auch nicht dafür entschieden, weil es hilfreich ist, das Objekt zu erstellen, dann die Verbindungszeichenfolge oder andere Kleinigkeiten zu optimieren und sie später zu verbinden .
Jimmy Hoffa
19

Pfui.

Ein Konstruktor sollte so wenig wie möglich tun - das Problem besteht darin, dass das Ausnahmeverhalten in Konstruktoren umständlich ist. Nur sehr wenige Programmierer kennen das richtige Verhalten (einschließlich Vererbungsszenarien), und Ihre Benutzer dazu zu zwingen, jede einzelne Instanziierung zu versuchen / abzufangen, ist im besten Fall ... schmerzhaft.

Es gibt zwei Teile dazu in dem Konstruktor und mit dem Konstruktor. Es ist im Konstruktor umständlich, weil Sie dort nicht viel tun können, wenn eine Ausnahme auftritt. Sie können keinen anderen Typ zurückgeben. Sie können nicht null zurückgeben. Grundsätzlich können Sie die Ausnahme erneut auslösen, ein defektes Objekt (fehlerhafter Konstruktor) zurückgeben oder (im besten Fall) einen internen Teil durch einen vernünftigen Standard ersetzen. Die Verwendung des Konstruktors ist umständlich, da dann Ihre Instantiierungen (und die Instantiierungen aller abgeleiteten Typen!) Ausgelöst werden können. Dann können Sie sie nicht in Elementinitialisierern verwenden. Sie verwenden am Ende Ausnahmen für die Logik, um zu sehen, ob meine Erstellung erfolgreich war. Dies geht über die Lesbarkeit (und Zerbrechlichkeit) hinaus, die durch try / catch überall verursacht wird.

Wenn Ihr Konstruktor nicht triviale Dinge ausführt oder Dinge, von denen vernünftigerweise erwartet werden kann, dass sie fehlschlagen, sollten Sie Createstattdessen eine statische Methode (mit einem nicht öffentlichen Konstruktor) in Betracht ziehen . Es ist viel benutzerfreundlicher, einfacher zu debuggen und bietet Ihnen bei Erfolg eine vollständig konstruierte Instanz.

Telastyn
quelle
2
Komplexe Konstruktionsszenarien sind genau der Grund, warum es Kreationsmuster gibt, wie Sie sie hier erwähnen. Dies ist ein guter Ansatz für diese problematischen Konstruktionen. Ich muss darüber nachdenken, wie in .NET ein ConnectionObjekt CreateCommandfür Sie funktionieren kann , damit Sie wissen, dass der Befehl ordnungsgemäß verbunden ist.
Jimmy Hoffa
2
Dazu möchte ich hinzufügen, dass eine Datenbank eine starke externe Abhängigkeit darstellt. Angenommen, Sie möchten zu einem späteren Zeitpunkt die Datenbank wechseln (oder ein Objekt zum Testen verspotten) und diese Art von Code in eine Factory-Klasse (oder so etwas wie einen IService) verschieben Anbieter) scheint wie ein sauberer Ansatz
Jason Sperske
4
Können Sie näher auf "Ausnahmeverhalten in Konstruktoren ist umständlich" eingehen? Wenn Sie Create verwenden würden, müssten Sie es nicht genauso in try catch einbinden, wie Sie einen Aufruf an einen Konstruktor einbinden würden? Ich glaube, dass Create nur ein anderer Name für den Konstruktor ist. Vielleicht bekomme ich Ihre Antwort nicht richtig?
Taein Kim
1
Nachdem ich versuchen musste, Ausnahmen zu finden, die von Konstruktoren ausgelöst wurden, +1 auf die Argumente dagegen. Wenn Sie mit Leuten gearbeitet haben, die sagen "Keine Arbeit im Konstruktor", kann dies auch zu seltsamen Mustern führen. Ich habe Code gesehen, in dem jedes Objekt nicht nur einen Konstruktor, sondern auch eine Art Initialize () hat. Das Objekt ist erst dann wirklich gültig, wenn es aufgerufen wird. Und dann müssen Sie zwei Dinge aufrufen: ctor und Initialize (). Das Problem der Arbeit zum Einrichten eines Objekts verschwindet nicht - C # liefert möglicherweise andere Lösungen als beispielsweise C ++. Fabriken sind gut!
J Trana
1
@ jtrana - ja, initialisieren ist nicht gut. Der Konstruktor wird auf einen vernünftigen Standardwert oder eine Factory initialisiert oder eine Erstellungsmethode erstellt und gibt eine verwendbare Instanz zurück.
Telastyn
1

Das Gleichgewicht zwischen Konstruktor und Methodenkomplexität ist in der Tat eine Frage des guten Stils. Sehr oft wird ein leerer Konstruktor Class() {}verwendet, mit einer Methode Init(..) {..}für kompliziertere Dinge. Zu berücksichtigende Argumente:

  • Möglichkeit der Klassenserialisierung
  • Bei der Trennung der Init-Methode vom Konstruktor müssen Sie häufig dieselbe Sequenz Class x = new Class(); x.Init(..);mehrmals wiederholen , teilweise in Komponententests. Nicht so gut, aber glasklar zum Lesen. Manchmal füge ich sie einfach in eine Codezeile ein.
  • Wenn Unit Test nur einen Klasseninitialisierungstest zum Ziel hat, ist es besser, einen eigenen Init zu haben, um kompliziertere Aktionen auszuführen, die einzeln mit den dazwischen liegenden Asserts ausgeführt werden.
Pavel Khrapkin
quelle
Meiner Meinung nach hat die initMethode den Vorteil, dass die Handhabung von Fehlern viel einfacher ist. Insbesondere kann der Rückgabewert der initMethode angeben, ob die Initialisierung erfolgreich ist, und die Methode kann sicherstellen, dass sich das Objekt immer in einem guten Zustand befindet.
pqnet