Soll ich ein Objekt an einen Konstruktor übergeben oder in der Klasse instanziieren?

10

Betrachten Sie diese beiden Beispiele:

Übergeben eines Objekts an einen Konstruktor

class ExampleA
{
  private $config;
  public function __construct($config)
  {
     $this->config = $config;
  }
}
$config = new Config;
$exampleA = new ExampleA($config);

Eine Klasse instanziieren

class ExampleB
{
  private $config;
  public function __construct()
  {
     $this->config = new Config;
  }
}
$exampleA = new ExampleA();

Wie gehe ich beim Hinzufügen eines Objekts als Eigenschaft richtig vor? Wann sollte ich eins über das andere verwenden? Beeinflusst Unit-Tests, was ich verwenden soll?

Häftling
quelle
Könnte dies auf codereview.stackexchange.com/questions besser sein ?
FrustratedWithFormsDesigner
9
@FrustratedWithFormsDesigner - Dies ist nicht für die Codeüberprüfung geeignet.
ChrisF

Antworten:

14

Ich denke, der erste gibt Ihnen die Möglichkeit, ein configObjekt an anderer Stelle zu erstellen und an dieses weiterzugeben ExampleA. Wenn Sie eine Abhängigkeitsinjektion benötigen, kann dies eine gute Sache sein, da Sie sicherstellen können, dass alle Intances dasselbe Objekt verwenden.

Auf der anderen Seite ExampleA benötigen Sie möglicherweise ein neues und sauberes configObjekt, sodass es Fälle geben kann, in denen das zweite Beispiel besser geeignet ist, z. B. Fälle, in denen jede Instanz möglicherweise eine andere Konfiguration hat.

FrustratedWithFormsDesigner
quelle
1
Also, A ist gut für Unit-Tests, B ist gut für andere Umstände ... warum nicht beides? Ich habe oft zwei Konstruktoren, einen für manuelle DI, einen für den normalen Gebrauch (ich verwende Unity für DI, das Konstruktoren generiert, um die DI-Anforderungen zu erfüllen).
Ed James
3
@ EdWoodcock Es geht nicht wirklich um Unit-Tests im Vergleich zu anderen Umständen. Das Objekt benötigt entweder die Abhängigkeit von außen oder verwaltet sie von innen. Niemals beides.
MattDavey
9

Vergessen Sie nicht die Testbarkeit!

Wenn das Verhalten der ExampleKlasse von der Konfiguration abhängt, möchten Sie sie normalerweise testen können, ohne den internen Status der Klasseninstanz zu speichern / zu ändern (dh Sie möchten einfache Tests für den Pfad "Happy Path" und für die falsche Konfiguration durchführen, ohne die Eigenschaft / zu ändern). Mitglied der ExampleKlasse).

Daher würde ich mit der ersten Option von gehen ExampleA

Paul
quelle
Deshalb habe ich das Testen erwähnt - danke, dass
Gefangener
5

Wenn das Objekt für die Verwaltung der Lebensdauer der Abhängigkeit verantwortlich ist, können Sie das Objekt im Konstruktor erstellen (und im Destruktor entsorgen). *

Wenn das Objekt nicht für die Verwaltung der Lebensdauer der Abhängigkeit verantwortlich ist, sollte es an den Konstruktor übergeben und von außen verwaltet werden (z. B. von einem IoC-Container).

In diesem Fall sollte Ihre ClassA meines Erachtens nicht die Verantwortung für die Erstellung von $ config übernehmen, es sei denn, sie ist auch für die Entsorgung verantwortlich oder die Konfiguration ist für jede Instanz von ClassA eindeutig.

* Um die Testbarkeit zu verbessern, kann der Konstruktor auf eine Factory-Klasse / -Methode verweisen, um die Abhängigkeit in seinem Konstruktor zu konstruieren, wodurch die Kohäsion und Testbarkeit erhöht wird.

//Object which manages the lifetime of its dependency (C#):
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA()
    {
        this.Config = new Config(); // Tightly coupled to Config class...
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

// Object which does not manage its dependency:
public class ClassA
{
    public Config Config { get; set; }

    public ClassA(Config config) // dependency may be injected...
    {
        this.Config = config;
    }
}

// Object which manages its dependency in a testable way:
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA(IConfigFactory configFactory) // dependency may be mocked...
    {
        this.Config = configFactory.BuildConfig();
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}
MattDavey
quelle
2

Ich habe kürzlich genau die gleiche Diskussion mit unserem Architektenteam geführt, und es gibt einige subtile Gründe, dies auf die eine oder andere Weise zu tun. Es kommt hauptsächlich auf die Abhängigkeitsinjektion an (wie andere angemerkt haben) und darauf, ob Sie wirklich die Kontrolle über die Erstellung des Objekts haben, das Sie neu im Konstruktor haben.

Was ist in Ihrem Beispiel, wenn Ihre Config-Klasse:

a) hat eine nicht triviale Zuordnung, z. B. aus einem Pool oder einer Fabrikmethode.

b) könnte nicht zugewiesen werden. Durch die Übergabe an den Konstruktor wird dieses Problem vermieden.

c) ist eigentlich eine Unterklasse von Config.

Die Übergabe des Objekts an den Konstruktor bietet die größte Flexibilität.

JBRWilkinson
quelle
1

Die Antwort unten ist falsch, aber ich werde sie behalten, damit andere daraus lernen können (siehe unten).

In ExampleAkönnen Sie dieselbe ConfigInstanz für mehrere Klassen verwenden. Sollte es jedoch nur eine ConfigInstanz in der gesamten Anwendung geben, sollten Sie das Singleton-Muster anwenden Config, um zu vermeiden, dass mehrere Instanzen von vorhanden sind Config. Und wenn Configes sich um einen Singleton handelt, können Sie stattdessen Folgendes tun:

class ExampleA
{
  private $config;
  public function __construct()
  {
     $this->config = Config->getInstance();
  }
}
$exampleA = new ExampleA();

In ExampleB, auf der anderen Seite finden Sie immer eine separate Instanz erhalten Configfür jede Instanz ExampleB.

Welche Version Sie wirklich anwenden sollten, hängt davon ab, wie die Anwendung Instanzen von Folgendem behandelt Config:

  • Wenn jede Instanz von ExampleXeine separate Instanz von haben sollte Config, gehen Sie mit ExampleB;
  • Wenn jede Instanz von ExampleXeine (und nur eine) Instanz von gemeinsam Confignutzt, verwenden Sie ExampleA with Config Singleton;
  • Wenn Instanzen von ExampleXunterschiedliche Instanzen von verwenden können Config, bleiben Sie bei ExampleA.

Warum die Konvertierung Configin einen Singleton falsch ist:

Ich muss zugeben, dass ich erst gestern etwas über das Singleton- Muster gelernt habe (Lesen des Head First- Buches mit Designmustern). Naiv ging ich herum und wandte es für dieses Beispiel an, aber wie viele darauf hingewiesen haben, ist ein Weg ein anderer (einige waren kryptischer und sagten nur "Du machst es falsch!"), Dies ist keine gute Idee. Um zu verhindern, dass andere den gleichen Fehler machen, den ich gerade gemacht habe, folgt eine Zusammenfassung, warum das Singleton- Muster schädlich sein kann (basierend auf den Kommentaren und dem, was ich beim Googeln herausgefunden habe):

  1. Wenn ExampleAein eigener Verweis auf die ConfigInstanz abgerufen wird , werden die Klassen eng gekoppelt. Es wird keine Möglichkeit geben, eine Instanz ExampleAzu haben, um eine andere Version von Config(sagen wir einige Unterklassen) zu verwenden. Dies ist schrecklich, wenn Sie ExampleAeine Mock-up-Instanz von testen möchten, Configda es keine Möglichkeit gibt, sie bereitzustellen ExampleA.

  2. Die Prämisse davon wird es eine und nur eine Instanz geben, die Configvielleicht jetzt gilt , aber Sie können nicht immer sicher sein, dass dies auch in Zukunft der Fall sein wird . Wenn sich zu einem späteren Zeitpunkt herausstellt, dass mehrere Instanzen von Configwünschenswert sind, gibt es keine Möglichkeit, dies zu erreichen, ohne den Code neu zu schreiben.

  3. Auch wenn die einzige Instanz von Configmöglicherweise für alle Ewigkeit gilt, kann es vorkommen, dass Sie eine Unterklasse von verwenden möchten Config(während Sie immer noch nur eine Instanz haben). Da der Code die Instanz jedoch direkt über getInstance()of abruft Config, was eine staticMethode ist, gibt es keine Möglichkeit, die Unterklasse abzurufen. Auch hier muss der Code neu geschrieben werden.

  4. Die Tatsache, dass ExampleAverwendet Configwird, wird verborgen, zumindest wenn nur die API von angezeigt wird ExampleA. Das mag eine schlechte Sache sein oder auch nicht, aber ich persönlich fühle, dass dies ein Nachteil ist. Bei der Wartung gibt es beispielsweise keine einfache Möglichkeit, herauszufinden, welche Klassen von Änderungen betroffen sind, Configohne die Implementierung jeder anderen Klasse zu untersuchen.

  5. Auch wenn die Tatsache, dass ExampleAein Singleton verwendet Config wird, an sich kein Problem darstellt, kann es aus Testsicht immer noch zu einem Problem werden. Singleton- Objekte tragen den Status, der bis zur Beendigung der Anwendung bestehen bleibt. Dies kann ein Problem sein, wenn Unit-Tests ausgeführt werden, da ein Test von einem anderen isoliert werden soll (dh, dass die Ausführung eines Tests das Ergebnis eines anderen Tests nicht beeinflussen sollte). Um dies zu beheben, muss das Singleton- Objekt zwischen jedem Testlauf zerstört werden (möglicherweise muss die gesamte Anwendung neu gestartet werden), was zeitaufwändig sein kann (ganz zu schweigen von langwierig und ärgerlich).

Trotzdem bin ich froh, dass ich diesen Fehler hier gemacht habe und nicht bei der Implementierung einer echten Anwendung. Tatsächlich dachte ich darüber nach, meinen neuesten Code neu zu schreiben, um das Singleton- Muster für einige der Klassen zu verwenden. Obwohl ich die Änderungen leicht rückgängig machen könnte (natürlich ist alles in einem SVN gespeichert), hätte ich trotzdem Zeit damit verschwendet.

gablin
quelle
4
Ich würde es nicht empfehlen, dies zu tun. Auf diese Weise koppeln Sie die Klasse eng ExampleAund Config- was nicht gut ist.
Paul
@ Paul: Das stimmt. Guter Fang, habe nicht darüber nachgedacht.
Gablin
3
Ich würde aus Gründen der Testbarkeit immer davon abraten, Singletons zu verwenden. Sie sind im Wesentlichen globale Variablen und es ist unmöglich, die Abhängigkeit zu verspotten.
MattDavey
4
Ich würde immer wieder empfehlen, Singletons zu verwenden, weil Sie es falsch machen.
Raynos
1

Die einfachste Sache zu tun ist , zu koppeln ExampleAzu Config. Sie sollten das Einfachste tun, es sei denn, es gibt einen zwingenden Grund, etwas Komplexeres zu tun.

Ein Grund für die Entkopplung ExampleAund Configwäre die Verbesserung der Testbarkeit von ExampleA. Direkte Kopplung wird die Prüfbarkeit degradieren , ExampleAwenn Confighat Methoden , die langsam, komplex sind oder sich schnell entwickelnden. Zum Testen ist eine Methode langsam, wenn sie länger als einige Mikrosekunden läuft. Wenn alle Methoden Configsind einfach und schnell, dann würde ich den einfachen Ansatz nehmen und direkt Paar ExampleAzu Config.

Kevin Cline
quelle
1

Ihr erstes Beispiel ist ein Beispiel für das Abhängigkeitsinjektionsmuster. Eine Klasse mit einer externen Abhängigkeit erhält die Abhängigkeit vom Konstruktor, Setter usw.

Dieser Ansatz führt zu lose gekoppeltem Code. Die meisten Leute denken, dass lose Kopplung eine gute Sache ist, da Sie die Konfiguration leicht ersetzen können, wenn eine bestimmte Instanz eines Objekts anders als die anderen konfiguriert werden muss. Sie können ein Scheinkonfigurationsobjekt zum Testen übergeben und so weiter auf.

Der zweite Ansatz ist näher am GRASP-Erstellungsmuster. In diesem Fall erstellt das Objekt seine eigenen Abhängigkeiten. Dies führt zu eng gekoppeltem Code. Dies kann die Flexibilität der Klasse einschränken und das Testen erschweren. Wenn Sie eine Instanz einer Klasse benötigen, um eine andere Abhängigkeit als die anderen zu haben, können Sie sie nur in Unterklassen unterteilen.

Dies kann jedoch das geeignete Muster sein, wenn die Lebensdauer des abhängigen Objekts durch die Lebensdauer des abhängigen Objekts bestimmt wird und das abhängige Objekt außerhalb des davon abhängigen Objekts nicht verwendet wird. Normalerweise würde ich DI raten, die Standardposition zu sein, aber Sie müssen den anderen Ansatz nicht vollständig ausschließen, solange Sie sich seiner Konsequenzen bewusst sind.

GordonM
quelle
0

Wenn Ihre Klasse die nicht $configfür externe Klassen verfügbar macht, würde ich sie im Konstruktor erstellen. Auf diese Weise halten Sie Ihren internen Status privat.

Wenn die $configAnforderung erfordert, dass der eigene interne Status vor der Verwendung ordnungsgemäß festgelegt wird (z. B. eine Datenbankverbindung oder einige interne Felder initialisiert werden müssen), ist es sinnvoll, die Initialisierung auf einen externen Code (möglicherweise eine Factory-Klasse) zu verschieben und ihn einzufügen der Konstruktor. Oder, wie andere betont haben, wenn es unter anderen Objekten geteilt werden muss.

TMN
quelle
0

Beispiel A ist von der konkreten Klasse Config entkoppelt, was gut ist , vorausgesetzt, das empfangene Objekt ist nicht vom Typ Config, sondern vom Typ einer abstrakten Superklasse von Config.

Beispiel B ist stark an die konkrete Klasse Config gekoppelt, was schlecht ist .

Das Instanziieren eines Objekts erzeugt eine starke Kopplung zwischen Klassen. Dies sollte in einer Factory-Klasse erfolgen.

Tulains Córdova
quelle