Abhängigkeitsinjektion & Singleton-Entwurfsmuster

86

Wie identifizieren wir, wann Abhängigkeitsinjektion oder Singleton-Muster verwendet werden sollen? Ich habe auf vielen Websites gelesen, dass dort "Abhängigkeitsinjektion über Singleton-Muster verwenden" steht. Aber ich bin mir nicht sicher, ob ich ihnen vollkommen zustimme. Für meine kleinen oder mittleren Projekte sehe ich die Verwendung von Singleton-Mustern definitiv unkompliziert.

Zum Beispiel Logger. Ich könnte verwenden. Logger.GetInstance().Log(...) Aber warum muss ich stattdessen jede von mir erstellte Klasse mit der Instanz des Loggers injizieren?

SysAdmin
quelle

Antworten:

64

Wenn Sie überprüfen möchten, was in einem Test protokolliert wird, benötigen Sie eine Abhängigkeitsinjektion. Darüber hinaus ist ein Logger selten ein Singleton - im Allgemeinen haben Sie einen Logger pro Klasse.

Sehen Sie sich diese Präsentation zum objektorientierten Design an, um die Testbarkeit zu überprüfen, und Sie werden sehen, warum Singletons schlecht sind.

Das Problem mit Singletons ist, dass sie einen globalen Zustand darstellen, der insbesondere bei Tests schwer vorherzusagen ist.

Denken Sie daran, dass ein Objekt de facto Singleton sein kann, aber dennoch über Abhängigkeitsinjektion und nicht über erhalten werden kann Singleton.getInstance().

Ich liste nur einige wichtige Punkte auf, die Misko Hevery in seiner Präsentation gemacht hat. Nachdem sie beobachten werden Sie auf volle Perspektive zu gewinnen , warum es besser ist , eine Aufgabe zu haben , zu definieren , was seine Abhängigkeiten sind, aber nicht einen Weg definieren , wie schaffen sie.

Bozho
quelle
Ich bin neu in diesem Designmuster und möchte wissen, warum Sie mit "Test" meinen, wenn Sie besonders in Tests sagen? Danke im Voraus.
Anis
89

Singletons sind wie Kommunismus: Beide klingen auf dem Papier großartig, explodieren aber in der Praxis vor Problemen.

Das Singleton-Muster legt einen unverhältnismäßigen Schwerpunkt auf den einfachen Zugriff auf Objekte. Der Kontext wird vollständig vermieden, da jeder Verbraucher ein Objekt mit AppDomain-Gültigkeitsbereich verwenden muss und keine Optionen für unterschiedliche Implementierungen übrig bleiben. Es bettet Infrastrukturwissen in Ihre Klassen ein (den Aufruf zu GetInstance()) und fügt gleichzeitig genau null Ausdruckskraft hinzu. Es verringert tatsächlich Ihre Ausdruckskraft, da Sie die von einer Klasse verwendete Implementierung nicht ändern können, ohne sie für alle zu ändern . Sie können einfach keine einmaligen Funktionen hinzufügen.

Wenn die Klasse Foodavon abhängt Logger.GetInstance(), Foowerden ihre Abhängigkeiten effektiv vor den Verbrauchern verborgen. Dies bedeutet, dass Sie es nur dann vollständig verstehen Foooder verwenden können, wenn Sie die Quelle gelesen und die Tatsache aufgedeckt haben, dass es davon abhängt Logger. Wenn Sie nicht über die Quelle verfügen, wird dadurch eingeschränkt, wie gut Sie den Code verstehen und effektiv verwenden können, von dem Sie abhängig sind.

Das mit statischen Eigenschaften / Methoden implementierte Singleton-Muster ist kaum mehr als ein Hack zur Implementierung einer Infrastruktur. Es schränkt Sie auf vielfältige Weise ein und bietet keinen erkennbaren Nutzen gegenüber den Alternativen. Sie können es verwenden, wie Sie möchten, aber da es praktikable Alternativen gibt, die ein besseres Design fördern, sollte es niemals eine empfohlene Vorgehensweise sein.

Bryan Watts
quelle
9
@BryanWatts alles, was gesagt wird, Singletons sind immer noch viel schneller und weniger fehleranfällig in einer normalen mittelgroßen Anwendung. Normalerweise habe ich nicht mehr als eine mögliche Implementierung für einen (benutzerdefinierten) Logger. Warum sollte ich also ** benötigen: 1. Erstellen Sie eine Schnittstelle dafür und ändern Sie sie jedes Mal, wenn ich öffentliche Mitglieder hinzufügen / ändern muss. 2. Pflegen eine DI-Konfiguration 3. Verbergen Sie die Tatsache, dass es im gesamten System ein einzelnes Objekt dieses Typs gibt. 4. Binden Sie sich an eine strenge Funktionalität, die durch eine vorzeitige Trennung von Bedenken verursacht wird.
Uri Abramson
@UriAbramson: Es scheint, dass Sie bereits die Entscheidung über die von Ihnen bevorzugten Kompromisse getroffen haben, also werde ich nicht versuchen, Sie zu überzeugen.
Bryan Watts
@BryanWatts Es ist nicht der Fall, vielleicht war mein Kommentar etwas zu hart, aber es interessiert mich tatsächlich sehr, zu hören, was Sie über den Punkt zu sagen haben, den ich angesprochen habe ...
Uri Abramson
1
@ UriAbramson: Fair genug. Würden Sie zumindest zustimmen, dass der Austausch von Implementierungen gegen Isolation während des Testens wichtig ist?
Bryan Watts
5
Die Verwendung von Singletons und die Abhängigkeitsinjektion schließen sich nicht gegenseitig aus. Ein Singleton kann eine Schnittstelle implementieren und daher verwendet werden, um eine Abhängigkeit von einer anderen Klasse zu erfüllen. Die Tatsache, dass es sich um ein Singleton handelt, zwingt nicht jeden Verbraucher, eine Referenz über die Methode / Eigenschaft "GetInstance" abzurufen.
Oliver
18

Andere haben das Problem mit Singletons im Allgemeinen sehr gut erklärt. Ich möchte nur einen Hinweis zum speziellen Fall von Logger hinzufügen. Ich stimme Ihnen zu, dass es normalerweise kein Problem ist, als Singleton über eine statische Methode getInstance()oder eine getRootLogger()Methode auf einen Logger (oder genauer gesagt auf den Root-Logger) zuzugreifen . (es sei denn, Sie möchten sehen, was von der Klasse, die Sie testen, protokolliert wird - aber meiner Erfahrung nach kann ich mich kaum an solche Fälle erinnern, in denen dies erforderlich war. Andererseits könnte dies für andere ein dringlicheres Problem sein).

IMO ist normalerweise ein Singleton-Logger kein Problem, da er keinen Status enthält, der für die zu testende Klasse relevant ist. Das heißt, der Status des Loggers (und seine möglichen Änderungen) haben keinerlei Auswirkungen auf den Status der getesteten Klasse. Dies erschwert Ihre Unit-Tests nicht.

Die Alternative wäre, den Logger über den Konstruktor in (fast) jede einzelne Klasse in Ihrer App zu injizieren. Aus Gründen der Konsistenz der Schnittstellen sollte es auch dann eingefügt werden, wenn die betreffende Klasse derzeit nichts protokolliert. Die Alternative wäre, dass Sie, wenn Sie irgendwann feststellen, dass Sie jetzt etwas aus dieser Klasse protokollieren müssen, einen Logger benötigen Sie müssen einen Konstruktorparameter für DI hinzufügen, der den gesamten Clientcode bricht. Ich mag diese beiden Optionen nicht und ich bin der Meinung, dass die Verwendung von DI für die Protokollierung mein Leben nur komplizieren würde, um einer theoretischen Regel ohne konkreten Nutzen zu entsprechen.

Mein Fazit lautet also: Eine Klasse, die (fast) universell verwendet wird, aber keinen für Ihre App relevanten Status enthält, kann sicher als Singleton implementiert werden .

Péter Török
quelle
1
Selbst dann kann ein Singleton ein Schmerz sein. Wenn Sie feststellen, dass Ihr Logger in einigen Fällen zusätzliche Parameter benötigt, können Sie entweder eine neue Methode erstellen (und den Logger hässlicher werden lassen) oder alle Verbraucher des Loggers brechen, auch wenn sie sich nicht darum kümmern.
Kyoryu
4
@kyoryu, ich spreche über den "üblichen" Fall, den IMHO impliziert, ein (de facto) Standard-Protokollierungsframework zu verwenden. (Die normalerweise über Eigenschaften- / XML-Dateien konfiguriert werden können.) Natürlich gibt es - wie immer - Ausnahmen. Wenn ich weiß, dass meine App in dieser Hinsicht außergewöhnlich ist, werde ich in der Tat keinen Singleton verwenden. Aber Überentwicklung, die sagt "das könnte irgendwann nützlich sein", ist fast immer eine vergebliche Anstrengung.
Péter Török
Wenn Sie DI bereits verwenden, ist es nicht viel zusätzliches Engineering. (Übrigens, ich bin nicht anderer Meinung als Sie, ich bin die Gegenstimme). Viele Logger benötigen einige "Kategorie" -Informationen oder ähnliches, und das Hinzufügen zusätzlicher Parameter kann Schmerzen verursachen. Das Ausblenden hinter einer Schnittstelle kann dazu beitragen, den verbrauchenden Code sauber zu halten und den Wechsel zu einem anderen Protokollierungsframework zu vereinfachen.
Kyoryu
1
@kyoryu, entschuldige die falsche Annahme. Ich habe eine Abwertung und einen Kommentar gesehen, also habe ich die Punkte verbunden - wie sich herausstellt, auf die falsche Weise :-( Ich habe noch nie die Notwendigkeit erlebt, zu einem anderen Protokollierungsframework zu wechseln, aber andererseits verstehe ich, dass dies legitim sein kann Bedenken in einigen Projekten.
Péter Török
5
Korrigieren Sie mich, wenn ich falsch liege, aber der Singleton wäre für die LogFactory, nicht für den Logger. Außerdem ist die LogFactory wahrscheinlich das Apache Commons oder die Slf4j-Protokollierungsfassade. Das Wechseln der Protokollierungsimplementierungen ist daher ohnehin problemlos. Ist der eigentliche Schmerz bei der Verwendung von DI zum Injizieren einer LogFactory nicht die Tatsache, dass Sie jetzt zum applicationContext gehen müssen, um jede Instanz in Ihrer App zu erstellen?
HDave
9

Es geht meistens, aber nicht vollständig um Tests. Singltons waren beliebt, weil es einfach war, sie zu konsumieren, aber Singletons haben eine Reihe von Nachteilen.

  • Schwer zu testen. Das heißt, wie stelle ich sicher, dass der Logger das Richtige tut.
  • Schwer zu testen. Das heißt, wenn ich Code teste, der den Logger verwendet, aber nicht im Mittelpunkt meines Tests steht, muss ich trotzdem sicherstellen, dass meine Testumgebung den Logger unterstützt
  • Manchmal möchte man keinen Singlton, sondern mehr Flexibilität

DI bietet Ihnen den einfachen Verbrauch Ihrer abhängigen Klassen - geben Sie es einfach in die Konstruktorargumente ein, und das System stellt es für Sie bereit - und bietet Ihnen gleichzeitig die Flexibilität beim Testen und Konstruieren.

Scott Weinstein
quelle
Nicht wirklich. Es geht um gemeinsame veränderbare Zustände und statische Abhängigkeiten, die die Dinge auf lange Sicht schmerzhaft machen. Testen ist nur das offensichtliche und häufig schmerzhafteste Beispiel.
Kyoryu
2

Das einzige Mal, dass Sie jemals einen Singleton anstelle von Dependency Injection verwenden sollten, ist, wenn der Singleton einen unveränderlichen Wert darstellt, wie z. B. List.Empty oder dergleichen (unter der Annahme unveränderlicher Listen).

Die Darmprüfung für einen Singleton sollte lauten: "Wäre ich in Ordnung, wenn dies eine globale Variable anstelle eines Singleton wäre?" Wenn nicht, verwenden Sie das Singleton-Muster, um eine globale Variable zu dokumentieren, und sollten einen anderen Ansatz in Betracht ziehen.

Kyoryu
quelle
1

Ich habe gerade den Monostate-Artikel gelesen - es ist eine raffinierte Alternative zu Singleton, aber es hat einige seltsame Eigenschaften:

class Mono{
    public static $db;
    public function setDb($db){
       self::$db = $db;
    }

}

class Mapper extends Mono{
    //mapping procedure
    return $Entity;

    public function save($Entity);//requires database connection to be set
}

class Entity{
public function save(){
    $Mapper = new Mapper();
    $Mapper->save($this);//has same static reference to database class     
}

$Mapper = new Mapper();
$Mapper->setDb($db);

$User = $Mapper->find(1);
$User->save();

Ist das nicht beängstigend - weil der Mapper wirklich von der Datenbankverbindung abhängig ist, um save () auszuführen -, aber wenn zuvor ein anderer Mapper erstellt wurde, kann er diesen Schritt beim Erfassen seiner Abhängigkeiten überspringen. Es ist zwar ordentlich, aber auch etwas chaotisch, nicht wahr?

Sunwukung
quelle