Ist die Verwendung von "new" im Konstruktor immer schlecht?

37

Ich habe gelesen, dass die Verwendung von "new" in einem Konstruktor (für andere Objekte als einfache Werte) eine schlechte Praxis ist, da dies das Testen von Einheiten unmöglich macht (da diese Mitbearbeiter ebenfalls erstellt werden müssen und nicht verspottet werden können). Da ich keine wirklichen Erfahrungen mit Unit-Tests habe, versuche ich, einige Regeln zu sammeln, die ich zuerst lernen werde. Ist dies auch eine allgemein gültige Regel, unabhängig von der verwendeten Sprache?

Ezoela Vacca
quelle
9
Es macht das Testen nicht unmöglich. Das Verwalten und Testen Ihres Codes wird dadurch jedoch schwieriger. Lesen Sie zum Beispiel " New Is Glue" .
David Arno
38
"immer" ist immer falsch. :) Es gibt Best Practices, aber es gibt viele Ausnahmen.
Paul
63
Welche Sprache ist das? newbedeutet verschiedene Dinge in verschiedenen Sprachen.
user2357112 unterstützt Monica
13
Diese Frage hängt ein bisschen von der Sprache ab, nicht wahr? Nicht alle Sprachen haben sogar ein newSchlüsselwort. Nach welchen Sprachen fragst du?
Bryan Oakley
7
Dumme Regel. Wenn Sie die Abhängigkeitsinjektion nicht verwenden, hat dies sehr wenig mit dem Schlüsselwort "new" zu tun. Anstatt zu sagen, dass "die Verwendung von new ein Problem darstellt, wenn Sie damit die Abhängigkeitsinversion aufheben", sagen Sie einfach "die Abhängigkeitsinversion nicht aufheben".
Matt Timmermans

Antworten:

36

Es gibt immer Ausnahmen, und ich habe Probleme mit dem "immer" im Titel, aber ja, diese Richtlinie ist allgemein gültig und gilt auch außerhalb des Konstruktors.

Die Verwendung von new in einem Konstruktor verletzt das D in SOLID (Prinzip der Abhängigkeitsinversion). Es erschwert das Testen Ihres Codes, da es beim Unit-Testen nur um Isolation geht. Es ist schwierig, Klassen zu isolieren, wenn sie konkrete Referenzen haben.

Es geht aber nicht nur um Unit-Tests. Was ist, wenn ich ein Repository auf zwei verschiedene Datenbanken gleichzeitig verweisen möchte? Die Möglichkeit, in meinem eigenen Kontext zu übergeben, ermöglicht es mir, zwei verschiedene Repositorys zu instanziieren, die auf unterschiedliche Speicherorte verweisen.

Wenn Sie new nicht im Konstruktor verwenden, wird der Code flexibler. Dies gilt auch für Sprachen, die andere Konstrukte als die newObjektinitialisierung verwenden können.

Es ist jedoch klar, dass Sie ein gutes Urteilsvermögen verwenden müssen. Es gibt viele Fälle, in denen die Verwendung in Ordnung newist oder in denen es besser ist, dies nicht zu tun, aber Sie werden keine negativen Konsequenzen haben. Irgendwann muss irgendwo newangerufen werden. Seien Sie nur vorsichtig, wenn Sie neweine Klasse aufrufen , von der viele andere Klassen abhängen.

Das Initialisieren einer leeren Privatsammlung in Ihrem Konstruktor ist in Ordnung und das Injizieren wäre absurd.

Je mehr Verweise eine Klasse darauf hat, desto sorgfältiger sollten Sie sein, nicht newaus ihr heraus aufzurufen .

TheCatWhisperer
quelle
12
Ich sehe wirklich keinen Wert in dieser Regel. Es gibt Regeln und Richtlinien, wie eine enge Kopplung im Code vermieden werden kann. Diese Regel scheint genau dasselbe Problem zu behandeln, außer dass sie unglaublich zu restriktiv ist und uns keinen zusätzlichen Wert gibt. Also, was ist der Punkt? Warum sollte ich ein Dummy-Header-Objekt für meine LinkedList-Implementierung erstellen, anstatt mich mit all den Sonderfällen zu befassen, die daraus resultieren, dass head = nullder Code in irgendeiner Weise verbessert wurde? Warum ist es besser, eingeschlossene Sammlungen als null zu belassen und sie bei Bedarf zu erstellen, als dies im Konstruktor zu tun?
Voo
22
Sie verpassen den Punkt. Ja, ein Persistenzmodul ist absolut etwas, von dem ein übergeordnetes Modul nicht abhängen sollte. "Niemals Neues verwenden" folgt jedoch nicht aus "Einige Dinge sollten injiziert und nicht direkt gekoppelt werden". Wenn Sie Ihre vertrauenswürdige Kopie von Agile Software Development nehmen, werden Sie feststellen, dass Martin im Allgemeinen von Modulen und nicht von einzelnen Klassen spricht: "Die übergeordneten Module behandeln die übergeordneten Richtlinien der Anwendung. Diese Richtlinien kümmern sich im Allgemeinen wenig um die Details, die implementiert werden Sie."
Voo
11
Der Punkt ist also, dass Sie Abhängigkeiten zwischen Modulen vermeiden sollten - insbesondere zwischen Modulen unterschiedlicher Abstraktionsebenen. Ein Modul kann jedoch mehr als eine einzelne Klasse sein, und es macht einfach keinen Sinn, Schnittstellen zwischen eng gekoppeltem Code derselben Abstraktionsschicht zu erstellen. In einer gut gestalteten Anwendung, die den SOLID-Prinzipien folgt, sollte nicht jede einzelne Klasse eine Schnittstelle implementieren, und nicht jeder Konstruktor sollte immer nur Schnittstellen als Parameter verwenden.
Voo
18
Ich habe abgelehnt, weil es eine Menge Modewortsuppe ist und nicht viel über praktische Überlegungen diskutiert wird. Es ist etwas an einem Repo für zwei DBs, aber ehrlich gesagt, das ist kein Beispiel aus der realen Welt.
jpmc26
5
@ jpmc26, BJoBnh Ohne darauf einzugehen, ob ich diese Antwort gutheiße oder nicht, wird der Punkt sehr klar ausgedrückt. Wenn Sie glauben, dass "Abhängigkeitsinversionsprinzip", "konkrete Referenz" oder "Objektinitialisierung" nur Schlagworte sind, dann wissen Sie einfach nicht, was sie bedeuten. Das ist nicht die Schuld der Antwort.
R. Schmitz
50

Obwohl ich es vorziehen würde, den Konstruktor nur zum Initialisieren der neuen Instanz zu verwenden, anstatt mehrere andere Objekte zu erstellen, sind Hilfsobjekte in Ordnung, und Sie müssen beurteilen, ob etwas ein solcher interner Helfer ist oder nicht.

Wenn die Klasse eine Auflistung darstellt, verfügt sie möglicherweise über ein internes Hilfsarray, eine Liste oder ein Hashset. Es würde verwendet new, um diese Helfer zu erstellen, und es würde als ganz normal angesehen. Die Klasse bietet keine Injektion an, um verschiedene interne Helfer zu verwenden, und hat keinen Grund dazu. In diesem Fall möchten Sie die öffentlichen Methoden des Objekts testen, die möglicherweise zum Akkumulieren, Entfernen und Ersetzen von Elementen in der Auflistung führen.


In gewissem Sinne ist das Klassenkonstrukt einer Programmiersprache ein Mechanismus zum Erzeugen von Abstraktionen höherer Ebenen, und wir erzeugen solche Abstraktionen, um die Lücke zwischen der Problemdomäne und den Programmiersprachenprimitiven zu schließen. Der Klassenmechanismus ist jedoch nur ein Werkzeug. Es variiert je nach Programmiersprache und für einige Domänenabstraktionen sind in einigen Sprachen einfach mehrere Objekte auf der Ebene der Programmiersprache erforderlich.

Zusammenfassend muss beurteilt werden, ob für die Abstraktion lediglich ein oder mehrere interne / Hilfsobjekte erforderlich sind, während der Aufrufer sie weiterhin als einzelne Abstraktion betrachtet, oder ob die anderen Objekte für den Aufrufer besser zugänglich sind, für den sie erstellt werden sollen Steuerung von Abhängigkeiten, die beispielsweise vorgeschlagen werden, wenn der Aufrufer diese anderen Objekte bei der Verwendung der Klasse sieht.

Erik Eidt
quelle
4
+1. Das ist genau richtig. Sie müssen das beabsichtigte Verhalten der Klasse identifizieren und bestimmen, welche Implementierungsdetails verfügbar gemacht werden müssen und welche nicht. Es gibt eine Art subtiles Intuitionsspiel, mit dem Sie herausfinden müssen, was die "Verantwortung" der Klasse ist.
jpmc26
27

Nicht alle Mitarbeiter sind interessant genug, um sie einzeln zu testen. Sie können sie (indirekt) durch die Hosting- / Instanziierungsklasse testen. Dies entspricht möglicherweise nicht der Vorstellung einiger Leute, dass sie jede Klasse, jede öffentliche Methode usw. testen müssen, insbesondere wenn sie danach testen. Wenn Sie TDD verwenden, können Sie diesen "Collaborator" überarbeiten, indem Sie eine Klasse extrahieren, deren Test bereits vollständig abgeschlossen ist.

Joppe
quelle
14
Not all collaborators are interesting enough to unit-test separatelyEnde der Geschichte :-), Dieser Fall ist möglich und niemand wagte es zu erwähnen. @Joppe Ich ermutige Sie, die Antwort ein wenig auszuarbeiten. Beispielsweise könnten Sie einige Beispiele für Klassen hinzufügen, die lediglich Implementierungsdetails darstellen (die nicht zum Ersetzen geeignet sind), und wie sie später extrahiert werden können, wenn wir dies für erforderlich halten.
Laiv
@Laiv-Domänenmodelle sind in der Regel konkret und nicht abstrakt. Sie müssen dort keine verschachtelten Objekte einfügen. Einfaches Wertobjekt / komplexes Objekt mit sonst keiner Logik sind ebenfalls ein Kandidat.
Joppe
4
+1, absolut. Wenn eine Sprache so eingerichtet ist , dass Sie haben zu nennen new File()etwas dateibezogenen zu tun, gibt es keinen Sinn, diesen Anruf zu verbieten. Was werden Sie tun, um Regressionstests für das stdlib- FileModul zu schreiben ? Unwahrscheinlich. Andererseits ist es zweifelhafter , neweine selbst erfundene Klasse anzurufen.
Kilian Foth
7
@KilianFoth: Viel Glück beim Testen von Gegenständen , die direkt anrufen new File().
Phoshi
1
Das ist eine Vermutung . Wir können Fälle finden, in denen es keinen Sinn ergibt, Fälle, in denen es nicht nützlich ist, und Fälle, in denen es Sinn ergibt und es nützlich ist. Es ist eine Frage der Bedürfnisse und Vorlieben.
Laiv
13

Da ich keine wirklichen Erfahrungen mit Unit-Tests habe, versuche ich, einige Regeln zu sammeln, die ich zuerst lernen werde.

Achten Sie darauf, "Regeln" für Probleme zu lernen, auf die Sie noch nie gestoßen sind. Wenn Sie auf eine "Regel" oder "Best Practice" stoßen, würde ich vorschlagen, ein einfaches Spielzeugbeispiel dafür zu finden, wo diese Regel "verwendet" werden soll, und zu versuchen, dieses Problem selbst zu lösen, und dabei zu ignorieren, was die "Regel" sagt.

In diesem Fall könnten Sie versuchen, zwei oder drei einfache Klassen und einige Verhaltensweisen zu finden, die sie implementieren sollten. Implementieren Sie die Klassen so, wie es sich für Sie natürlich anfühlt, und schreiben Sie für jedes Verhalten einen Komponententest. Erstellen Sie eine Liste der aufgetretenen Probleme, z. B. wenn Sie anfingen, auf eine Weise zu arbeiten, und diese später ändern mussten. wenn Sie sich nicht sicher sind, wie die Dinge zusammenpassen sollen; wenn Sie sich darüber geärgert haben, Kesselschild zu schreiben; etc.

Dann versuchen , das gleiche Problem zu lösen , indem Sie die „Regel“. Erstellen Sie erneut eine Liste der aufgetretenen Probleme. Vergleichen Sie die Listen und überlegen Sie, welche Situationen besser sind, wenn Sie die Regel befolgen, und welche nicht.


Was Ihre eigentliche Frage betrifft , neige ich dazu, einen Port- und Adapter- Ansatz zu bevorzugen , bei dem zwischen "Kernlogik" und "Diensten" unterschieden wird (dies ähnelt der Unterscheidung zwischen reinen Funktionen und effektiven Prozeduren).

Bei der Kernlogik geht es darum, Dinge "innerhalb" der Anwendung basierend auf der Problemdomäne zu berechnen. Es könnte enthält Klassen wie User, Document, Order, Invoice, etc. Es ist in Ordnung zu haben Kernklassen rufen newfür andere Kern - Klassen, da sie „intern“ Implementierungsdetails. Wenn Sie beispielsweise eine Ordererstellen, können Sie auch eine Invoiceund eine DocumentDetaillierung der Bestellung erstellen . Es ist nicht notwendig, diese während der Tests zu verspotten, da dies die tatsächlichen Dinge sind, die wir testen möchten!

Über die Ports und Adapter interagiert die Kernlogik mit der Außenwelt. Dies ist , wo Dinge wie Database, ConfigFile, EmailSenderusw. leben. Dies sind die Dinge, die das Testen schwierig machen. Daher ist es ratsam, diese außerhalb der Kernlogik zu erstellen und nach Bedarf weiterzugeben (entweder mit Abhängigkeitsinjektion oder als Methodenargumente usw.).

Auf diese Weise kann die Kernlogik (der anwendungsspezifische Teil, in dem sich die wichtige Geschäftslogik befindet und der die meisten Änderungen unterworfen sind) selbstständig getestet werden, ohne sich um Datenbanken, Dateien, E-Mails usw. kümmern zu müssen. Wir können nur einige Beispielwerte übergeben und überprüfen, ob wir die richtigen Ausgabewerte erhalten.

Die Ports und Adapter können separat mit Hilfe von Mocks für die Datenbank, das Dateisystem usw. getestet werden, ohne sich um die Geschäftslogik kümmern zu müssen. Wir können nur einige Beispielwerte übergeben und sicherstellen, dass sie gespeichert / gelesen / gesendet / usw. werden. passend.

Warbo
quelle
6

Lassen Sie mich die Frage beantworten und zusammenfassen, was ich hier für die wichtigsten Punkte halte. Ich werde einige Benutzer der Kürze halber zitieren.

Es gibt immer Ausnahmen, aber ja, diese Regel ist allgemein gültig und gilt auch außerhalb des Konstruktors.

Die Verwendung von new in einem Konstruktor verletzt das D in SOLID (Abhängigkeitsinversionsprinzip). Es erschwert das Testen Ihres Codes, da es beim Unit-Testen nur um Isolation geht. Es ist schwierig, Klassen zu isolieren, wenn sie konkrete Referenzen haben.

-TheCatWhisperer-

Ja, die Verwendung von newInnenkonstruktoren führt häufig zu Konstruktionsfehlern (z. B. dichter Kopplung), die unsere Konstruktion starr machen. Schwer zu testen , aber nicht unmöglich. Die Eigenschaft, die hier im Spiel ist, ist die Belastbarkeit (Toleranz gegenüber Änderungen) 1 .

Trotzdem stimmt das obige Zitat nicht immer. In einigen Fällen kann es Klassen geben , die eng miteinander verbunden sein sollen . David Arno hat ein paar kommentiert.

Es gibt natürlich Ausnahmen , bei denen die Klasse ist ein unveränderliches Wert Objekt, eine Implementierung Detail , usw. Wo sie sollen eng miteinander gekoppelt werden .

-David Arno-

Genau. Einige Klassen (zum Beispiel innere Klassen) können lediglich Implementierungsdetails der Hauptklasse sein. Diese sollen zusammen mit der Hauptklasse getestet werden und sind nicht unbedingt austauschbar oder erweiterbar.

Wenn wir diese Klassen durch unseren SOLID- Kult extrahieren , verletzen wir möglicherweise ein anderes gutes Prinzip. Das sogenannte Gesetz von Demeter . Was ich andererseits aus gestalterischer Sicht sehr wichtig finde.

Die wahrscheinliche Antwort hängt also , wie üblich, ab . Die Verwendung von newInside-Konstruktoren kann eine schlechte Praxis sein. Aber nicht immer systematisch.

Wir müssen also bewerten, ob es sich bei den Klassen um Implementierungsdetails der Hauptklasse handelt (in den meisten Fällen nicht). Wenn ja, lassen Sie sie in Ruhe. Wenn dies nicht der Fall ist , ziehen Sie Techniken wie Composition Root oder Dependency Injection von IoC-Containern in Betracht .


1: Das Hauptziel von SOLID ist es nicht, unseren Code testbarer zu machen. Es soll unseren Code toleranter für Änderungen machen. Flexibler und damit einfacher zu testen

Anmerkung: David Arno, TheWhisperCat, ich hoffe es macht Ihnen nichts aus, dass ich Sie zitiert habe.

Laiv
quelle
3

Betrachten Sie als einfaches Beispiel den folgenden Pseudocode

class Foo {
  private:
     class Bar {}
     class Baz inherits Bar {}
     Bar myBar
  public:
     Foo(bool useBaz) { if (useBaz) myBar = new Baz else myBar = new Bar; }
}

Da dies newein reines Implementierungsdetail von Foound beides ist Foo::Barund ein Foo::BazTeil davon ist Foo, hat es beim Testen von Einheiten Fookeinen Sinn, Teile von zu verspotten Foo. Sie verspotten die Teile nur im Freien, Foo wenn Sie die Einheit testen Foo.

MSalters
quelle
-3

Ja, die Verwendung von "new" in Ihren Anwendungsstammklassen ist ein Codegeruch. Dies bedeutet, dass Sie die Klasse an die Verwendung einer bestimmten Implementierung binden und keine andere ersetzen können. Entscheiden Sie sich immer für das Einfügen der Abhängigkeit in den Konstruktor. Auf diese Weise können Sie während des Testens nicht nur leicht verspottete Abhängigkeiten einfügen, sondern Ihre Anwendung wird auch viel flexibler, da Sie bei Bedarf schnell verschiedene Implementierungen austauschen können.

BEARBEITEN: Für Abwähler - hier ist ein Link zu einem Softwareentwicklungsbuch, das "neu" als möglichen Codegeruch kennzeichnet: https://books.google.com/books?id=18SuDgAAQBAJ&lpg=PT169&dq=new%20keyword%20code%20smell&pg=PT169 # v = onepage & q = new% 20keyword% 20code% 20smell & f = false

Eternal21
quelle
15
Yes, using 'new' in your non-root classes is a code smell. It means you are locking the class into using a specific implementation, and will not be able to substitute another.Warum ist das ein Problem? Nicht jede einzelne Abhängigkeit in einem Abhängigkeitsbaum sollte
ersetzbar sein
4
@Paul: Eine Standardimplementierung bedeutet, dass Sie eng an die als Standard angegebene konkrete Klasse gebunden sind. Das macht es jedoch nicht zu einem sogenannten "Code-Geruch".
Robert Harvey
8
@EzoelaVacca: Ich wäre vorsichtig, wenn ich die Wörter "Code-Geruch" in jedem Kontext verwenden würde. Es ist ein bisschen wie "Republikaner" oder "Demokrat" zu sagen; Was bedeuten diese Worte überhaupt? Sobald Sie so etwas etikettieren, denken Sie nicht mehr über die tatsächlichen Probleme nach, und das Lernen hört auf.
Robert Harvey
4
"Flexibler" ist nicht automatisch "besser".
Whatsisname
4
@EzoelaVacca: Die Verwendung des newSchlüsselworts ist keine schlechte Praxis und hat es auch nie getan . Es ist , wie Sie das Tool , dass Angelegenheiten verwenden. Sie verwenden beispielsweise keinen Vorschlaghammer, bei dem ein Kugelhammer ausreicht.
Robert Harvey