Soll ich Dependency Injection oder statische Fabriken verwenden?

81

Beim Entwerfen eines Systems stehe ich häufig vor dem Problem, dass eine Reihe von Modulen (Protokollierung, Datenbankzugriff usw.) von den anderen Modulen verwendet werden. Die Frage ist, wie ich diese Komponenten anderen Komponenten zur Verfügung stelle. Zwei Antworten erscheinen als mögliche Abhängigkeitsinjektion oder unter Verwendung des Factory-Musters. Beide scheinen jedoch falsch zu sein:

  • Fabriken machen das Testen zum Problem und ermöglichen kein einfaches Austauschen von Implementierungen. Sie machen auch keine Abhängigkeiten sichtbar (z. B. untersuchen Sie eine Methode, ohne zu bemerken, dass sie eine Methode aufruft, die eine Methode aufruft, die eine Methode aufruft, die eine Datenbank verwendet).
  • Die Dependecy-Injektion vergrößert Konstruktorargumentlisten massiv und verschmiert einige Aspekte im gesamten Code. Typischerweise sehen Konstrukteure von mehr als einer halben Klasse so aus(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

Hier ist eine typische Situation, mit der ich ein Problem habe: Ich habe Ausnahmeklassen, die Fehlerbeschreibungen verwenden, die aus der Datenbank geladen wurden. Dabei wird eine Abfrage verwendet, die Parameter für die Benutzerspracheneinstellung enthält und sich im Benutzersitzungsobjekt befindet. Um eine neue Ausnahme zu erstellen, benötige ich eine Beschreibung, die eine Datenbanksitzung und die Benutzersitzung erfordert. Daher bin ich dazu verdammt, all diese Objekte über alle meine Methoden zu ziehen, nur für den Fall, dass ich eine Ausnahme auslösen muss.

Wie gehe ich ein solches Problem an?

RokL
quelle
2
Wenn eine Factory all Ihre Probleme lösen kann, können Sie die Factory möglicherweise einfach in Ihre Objekte einfügen und daraus LoggingProvider, DbSessionProvider, ExceptionFactory und UserSession abrufen.
Giorgio
1
Zu viele "Eingaben" für eine Methode, ob sie übergeben oder injiziert werden, sind eher ein Problem des Methodendesigns. Was auch immer Sie mitnehmen, möchten Sie vielleicht die Größe Ihrer Methoden ein wenig reduzieren (was einfacher ist, wenn Sie die Injektion erst einmal erhalten haben)
Bill K
Die Lösung hier sollte nicht darin bestehen, die Argumente zu reduzieren. Erstellen Sie stattdessen Abstraktionen, die ein übergeordnetes Objekt erstellen, das die gesamte Arbeit im Objekt erledigt und Ihnen Vorteile bietet.
Alex

Antworten:

74

Verwenden Sie die Abhängigkeitsinjektion. Wenn Ihre Konstruktorargumentlisten jedoch zu umfangreich werden, können Sie sie mithilfe eines Facade-Service umgestalten . Die Idee ist, einige der Konstruktorargumente zu gruppieren und eine neue Abstraktion einzuführen.

Beispielsweise könnten Sie einen neuen Typ einführen , der SessionEnvironmentein DBSessionProvider, das UserSessionund das Geladene einkapselt Descriptions. Um zu wissen, welche Abstraktionen am sinnvollsten sind, müssen Sie jedoch die Details Ihres Programms kennen.

Eine ähnliche Frage wurde hier bereits zu SO gestellt .

Doc Brown
quelle
9
+1: Ich halte es für eine sehr gute Idee, Konstruktorargumente in Klassen zu gruppieren. Es zwingt Sie auch, diese Argumente in aussagekräftigeren Strukturen zu organisieren.
Giorgio
5
Wenn das Ergebnis jedoch keine aussagekräftigen Strukturen sind, verbergen Sie nur die Komplexität, die die SRP verletzt. In diesem Fall sollte ein Klassen-Refactoring durchgeführt werden.
Danidacar
1
@ Giorgio Ich bin mit der allgemeinen Aussage nicht einverstanden, dass "das Gruppieren von Konstruktorargumenten in Klassen eine sehr gute Idee ist." Wenn Sie dies mit "in diesem Szenario" qualifizieren, ist es anders.
Tymtam
19

Die Dependecy-Injektion vergrößert Konstruktorargumentlisten massiv und verschmiert einige Aspekte im gesamten Code.

Aus diesem Grund scheinen Sie DI nicht richtig zu verstehen - die Idee ist, das Objektinstanziierungsmuster innerhalb einer Fabrik zu invertieren.

Ihr spezielles Problem scheint ein allgemeineres OOP-Problem zu sein. Warum können die Objekte während der Laufzeit nicht einfach normale, nicht von Menschen lesbare Ausnahmen auslösen und haben dann etwas vor dem letzten Versuch / Fang, der diese Ausnahme abfängt, und verwenden an diesem Punkt die Sitzungsinformationen, um eine neue, schönere Ausnahme auszulösen ?

Ein anderer Ansatz wäre eine Exception-Factory, die über ihre Konstruktoren an die Objekte übergeben wird. Anstatt eine neue Ausnahme auszulösen, kann die Klasse eine Methode der Factory auslösen (z throw PrettyExceptionFactory.createException(data). B.

Denken Sie daran, dass Ihre Objekte, abgesehen von Ihren Werksobjekten, niemals den newOperator verwenden sollten. Ausnahmen sind in der Regel der einzige Sonderfall, in Ihrem Fall jedoch möglicherweise eine Ausnahme!

Jonathan Rich
quelle
1
Ich habe irgendwo gelesen, dass wenn Ihre Parameterliste zu lang wird, dies nicht daran liegt, dass Sie die Abhängigkeitsinjektion verwenden, sondern daran, dass Sie mehr Abhängigkeitsinjektion benötigen.
Giorgio
Dies könnte einer der Gründe sein - im Allgemeinen sollte Ihr Konstruktor abhängig von der Sprache nicht mehr als 6-8 Argumente haben und nicht mehr als 3-4 davon sollten Objekte selbst sein, es sei denn, das spezifische Muster (wie das BuilderMuster) diktiert es. Wenn Sie Parameter an Ihren Konstruktor übergeben, weil Ihr Objekt andere Objekte instanziiert, ist dies ein klarer Fall für IoC.
Jonathan Rich
12

Sie haben die Nachteile des statischen Fabrikmusters bereits recht gut aufgelistet, aber ich stimme den Nachteilen des Abhängigkeitsinjektionsmusters nicht ganz zu:

Diese Abhängigkeitsinjektion erfordert, dass Sie Code für jede Abhängigkeit schreiben. Dies ist kein Fehler, sondern eine Funktion: Sie müssen überlegen, ob Sie diese Abhängigkeiten wirklich benötigen, und fördern so die lose Kopplung. In deinem Beispiel:

Hier ist eine typische Situation, mit der ich ein Problem habe: Ich habe Ausnahmeklassen, die Fehlerbeschreibungen verwenden, die aus der Datenbank geladen wurden. Dabei wird eine Abfrage verwendet, die Parameter für die Benutzerspracheneinstellung enthält und sich im Benutzersitzungsobjekt befindet. Um eine neue Ausnahme zu erstellen, benötige ich eine Beschreibung, die eine Datenbanksitzung und die Benutzersitzung erfordert. Daher bin ich dazu verdammt, all diese Objekte über alle meine Methoden zu ziehen, nur für den Fall, dass ich eine Ausnahme auslösen muss.

Nein, du bist nicht zum Scheitern verurteilt. Warum liegt es in der Verantwortung der Geschäftslogik, Ihre Fehlermeldungen für eine bestimmte Benutzersitzung zu lokalisieren? Was wäre, wenn Sie irgendwann in der Zukunft diesen Business-Service aus einem Stapelverarbeitungsprogramm (das keine Benutzersitzung hat ...) verwenden möchten? Oder was ist, wenn die Fehlermeldung nicht dem aktuell angemeldeten Benutzer, sondern seinem Vorgesetzten (der möglicherweise eine andere Sprache bevorzugt) angezeigt werden soll? Oder was, wenn Sie Geschäftslogik auf dem Client wiederverwenden möchten (der keinen Zugriff auf eine Datenbank hat ...)?

Das Lokalisieren von Nachrichten hängt natürlich davon ab, wer diese Nachrichten ansieht, dh, es liegt in der Verantwortung der Präsentationsschicht. Daher würde ich normale Ausnahmen vom Business Service auslösen, die eine Nachrichtenkennung enthalten, die dann im Ausnahmehandler der Präsentationsschicht in der jeweils verwendeten Nachrichtenquelle nachgeschlagen werden kann.

Auf diese Weise können Sie 3 unnötige Abhängigkeiten (UserSession, ExceptionFactory und wahrscheinlich Beschreibungen) entfernen, wodurch Ihr Code einfacher und vielseitiger wird.

Im Allgemeinen würde ich statische Fabriken nur für Dinge verwenden, auf die Sie allgegenwärtigen Zugriff benötigen, und die garantiert in allen Umgebungen verfügbar sind, in denen wir den Code jemals ausführen möchten (z. B. Protokollierung). Für alles andere würde ich normale alte Abhängigkeitsinjektion verwenden.

Meriton - im Streik
quelle
Diese. Ich kann nicht erkennen, dass die DB getroffen werden muss , um eine Ausnahme auszulösen.
Caleth
1

Verwenden Sie die Abhängigkeitsinjektion. Die Verwendung statischer Fabriken ist eine Beschäftigung mit dem Service LocatorAntimuster. Die wegweisenden Arbeiten von Martin Fowler finden Sie hier - http://martinfowler.com/articles/injection.html

Wenn Ihre Konstruktorargumente zu groß werden und Sie keinesfalls einen DI-Container verwenden, schreiben Sie Ihre eigenen Factorys für die Instanziierung und lassen Sie diese entweder per XML oder durch Bindung einer Implementierung an eine Schnittstelle konfigurieren.

Sam
quelle
5
Service Locator ist kein Antimuster - Fowler selbst verweist in der von Ihnen geposteten URL darauf. Das Service Locator-Muster kann zwar missbraucht werden (genau wie Singletons - um den globalen Zustand zu beseitigen), es ist jedoch ein sehr nützliches Muster.
Jonathan Rich
1
Interessant zu wissen. Ich habe immer gehört, dass es als Anti-Muster bezeichnet wird.
Sam
4
Es ist nur ein Antipattern, wenn der Service Locator zum Speichern des globalen Status verwendet wird. Der Service-Locator sollte nach der Instantiierung ein zustandsloses Objekt sein und vorzugsweise unveränderlich.
Jonathan Rich
XML ist nicht typsicher. Ich würde es als einen Plan betrachten, nachdem jeder andere Ansatz fehlgeschlagen ist
Richard Tingle
1

Ich würde auch mit Abhängigkeitsinjektion gehen. Denken Sie daran, dass DI nicht nur über Konstruktoren, sondern auch über Eigenschaftssetzer erfolgt. Zum Beispiel könnte der Logger als eine Eigenschaft injiziert werden.

Möglicherweise möchten Sie auch einen IoC-Container verwenden, der Sie möglicherweise entlastet, z. B. indem Sie die Konstruktorparameter auf die zur Laufzeit von Ihrer Domänenlogik benötigten Werte beschränken (wobei der Konstruktor auf eine Weise beibehalten wird, die die Absicht von enthüllt) die Klassen- und die realen Domänenabhängigkeiten) und möglicherweise andere Hilfsklassen über Eigenschaften einschleusen.

Ein weiterer Schritt, den Sie vielleicht machen möchten, ist die aspektorientierte Programmierung, die in vielen wichtigen Frameworks implementiert ist. Auf diese Weise können Sie den Konstruktor der Klasse abfangen (oder zur Verwendung der AspectJ-Terminologie "raten") und die relevanten Eigenschaften einfügen, möglicherweise mit einem speziellen Attribut versehen.

Tallmaris
quelle
4
Ich vermeide DI durch Setter, weil es ein Zeitfenster einführt, in dem sich das Objekt nicht im vollständig initialisierten Zustand befindet (zwischen Konstruktor- und Setter-Aufruf). Mit anderen Worten, es wird eine Methodenaufrufreihenfolge eingeführt (muss X vor Y aufrufen), die ich meide, wenn es überhaupt möglich ist.
RokL
1
DI durch Eigenschaftssetzer ist ideal für optionale Abhängigkeiten. Protokollierung ist ein gutes Beispiel. Wenn Sie eine Protokollierung benötigen, legen Sie die Eigenschaft Logger fest. Wenn nicht, legen Sie sie nicht fest.
Preston
1

Fabriken machen das Testen zum Problem und ermöglichen kein einfaches Austauschen von Implementierungen. Sie machen auch keine Abhängigkeiten sichtbar (z. B. untersuchen Sie eine Methode, ohne zu bemerken, dass sie eine Methode aufruft, die eine Methode aufruft, die eine Methode aufruft, die eine Datenbank verwendet).

Da stimme ich nicht ganz zu Zumindest nicht im Allgemeinen.

Einfache fabrik:

public IFoo GetIFoo()
{
    return new Foo();
}

Einfache Injektion:

myDependencyInjector.Bind<IFoo>().To<Foo>();

Beide Schnipsel haben den gleichen Zweck, sie stellen eine Verbindung zwischen IFoound her Foo. Alles andere ist nur Syntax.

Das Ändern von Foozu ADifferentFooerfordert in beiden Codebeispielen genau so viel Aufwand.
Ich habe gehört, dass die Leute argumentieren, dass die Abhängigkeitsinjektion die Verwendung unterschiedlicher Bindungen zulässt, aber dasselbe Argument für die Herstellung unterschiedlicher Fabriken vorgebracht werden kann. Die Auswahl der richtigen Bindung ist genauso komplex wie die Auswahl der richtigen Fabrik.

Mit Factory-Methoden können Sie z. B. Fooan einigen Orten und ADifferentFooan anderen Orten verwenden. Einige nennen das gut (nützlich, wenn Sie es brauchen), andere nennen es schlecht (Sie könnten einen halbherzigen Job machen, wenn Sie alles ersetzen).
Es ist jedoch nicht IFooallzu schwer, diese Mehrdeutigkeit zu vermeiden, wenn Sie sich an eine einzelne Methode halten, die zurückgibt, sodass Sie immer eine einzige Quelle haben. Wenn Sie sich nicht in den Fuß schießen möchten, halten Sie entweder keine geladene Waffe oder zielen Sie nicht auf Ihren Fuß.


Die Dependecy-Injektion vergrößert Konstruktorargumentlisten massiv und verschmiert einige Aspekte im gesamten Code.

Aus diesem Grund ziehen es einige Leute vor, Abhängigkeiten im Konstruktor explizit abzurufen:

public MyService()
{
    _myFoo = DependencyFramework.Get<IFoo>();
}

Ich habe Argumente pro (kein Konstruktor aufgebläht) gehört, ich habe Argumente con (die Verwendung des Konstruktors ermöglicht mehr DI-Automatisierung) gehört.

Persönlich habe ich, während ich unserem Vorgesetzten nachgegeben habe, der Konstruktorargumente verwenden möchte, ein Problem mit der Dropdown-Liste in VS (oben rechts, um die Methoden der aktuellen Klasse zu durchsuchen) festgestellt, bei dem die Methodennamen in einem Fall außer Sichtweite geraten der Methodensignaturen ist länger als mein Bildschirm (=> der aufgeblähte Konstruktor).

Aus technischer Sicht ist mir das egal. Die Eingabe beider Optionen erfordert ungefähr den gleichen Aufwand. Und da Sie DI verwenden, rufen Sie einen Konstruktor normalerweise sowieso nicht manuell auf. Aber der Fehler in der Visual Studio-Benutzeroberfläche lässt mich das Konstruktorargument nicht aufblähen.


Nebenbei bemerkt, die Abhängigkeitsinjektion und die Fabriken schließen sich nicht gegenseitig aus . Es gab Fälle, in denen ich anstelle einer Abhängigkeit eine Factory eingefügt habe, die Abhängigkeiten generiert (NInject ermöglicht Ihnen zum Glück die Verwendung, Func<IFoo>sodass Sie keine tatsächliche Factory-Klasse erstellen müssen).

Die Anwendungsfälle hierfür sind selten, aber vorhanden.

Flater
quelle
OP fragt nach statischen Fabriken. stackoverflow.com/questions/929021/…
Basilevs
@Basilevs "Die Frage ist, wie ich diese Komponenten anderen Komponenten zur Verfügung stelle."
Anthony Rutledge
1
@Basilevs Alles, was ich gesagt habe, außer der Randnotiz, gilt auch für statische Fabriken. Ich bin nicht sicher, worauf Sie konkret hinweisen wollen. Worauf bezieht sich der Link?
Flater
Könnte ein solcher Anwendungsfall für das Injizieren einer Factory ein solcher Fall sein, in dem eine abstrakte HTTP-Anforderungsklasse erstellt und fünf weitere polymorphe Kindklassen strategisch festgelegt wurden, eine für GET, POST, PUT, PATCH und DELETE? Sie können nicht das HTTP - Methode wird jedes Mal wissen , verwendet werden , wenn sie versuchen , mit einem MVC - Typ Router die Klassenhierarchie zu integrieren (was teilweise auf einer HTTP - Anforderung Klasse verlassen kann PSR-7 HTTP - Message - Interface ist hässlich..
Anthony Rutledge
@AnthonyRutledge: DI-Fabriken können zwei Dinge bedeuten (1) Eine Klasse, die mehrere Methoden verfügbar macht. Ich denke, das ist es, wovon du sprichst. Dies ist jedoch für Fabriken nicht besonders. Ob es sich um eine Geschäftslogikklasse mit mehreren öffentlichen Methoden oder eine Factory mit mehreren öffentlichen Methoden handelt, ist eine Frage der Semantik und macht keinen technischen Unterschied. (2) Der DI-spezifische Anwendungsfall für Fabriken ist, dass eine Nicht-Fabrikabhängigkeit einmal (während der Injektion) instanziiert wird , während eine Fabrikversion verwendet werden kann, um die tatsächliche Abhängigkeit zu einem späteren Zeitpunkt (und möglicherweise mehrmals) zu instanziieren.
Flater
0

In diesem Beispiel wird zur Laufzeit eine Factory-Klasse verwendet, um basierend auf der HTTP-Anforderungsmethode zu bestimmen, welche Art von eingehendem HTTP-Anforderungsobjekt instanziiert werden soll. Die Fabrik selbst wird mit einer Instanz eines Abhängigkeitsinjektionsbehälters injiziert. Auf diese Weise kann die Factory die Laufzeit ermitteln und die Abhängigkeiten vom Abhängigkeitsinjektionscontainer verarbeiten lassen. Jedes eingehende HTTP-Anforderungsobjekt weist mindestens vier Abhängigkeiten auf (Superglobale und andere Objekte).

<?php
namespace TFWD\Factories;

/**
 * A class responsible for instantiating
 * InboundHttpRequest objects (PHP 7.x)
 * 
 * @author Anthony E. Rutledge
 * @version 2.0
 */
class InboundHttpRequestFactory 
{
    private const GET = 'GET';
    private const POST = 'POST';
    private const PUT = 'PUT';
    private const PATCH = 'PATCH';
    private const DELETE = 'DELETE';

    private static $di;
    private static $method;

    // public function __construct(Injector $di, Validator $httpRequestValidator)
    // {
    //    $this->di = $di;
    //    $this->method = $httpRequestValidator->getMethod();
    // }

    public static function setInjector(Injector $di)
    {
        self::$di = $di;
    }    

    public static setMethod(string $method)
    {
        self::$method = $method;
    }

    public static function getRequest()
    {
        if (self::$method == self::GET) {
            return self::$di->get('InboundGetHttpRequest');
        } elseif ((self::$method == self::POST) && empty($_FILES)) {
            return self::$di->get('InboundPostHttpRequest');
        } elseif (self::$method == self::POST) {
            return self::$di->get('InboundFilePostHttpRequest');
        } elseif (self::$method == self::PUT) {
            return self::$di->get('InboundPutHttpRequest');
        } elseif (self::$method == self::PATCH) {
            return self::$di->get('InboundPatchHttpRequest');
        } elseif (self::$method == self::DELETE) {
            return self::$di->get('InboundDeleteHttpRequest');
        } else {
            throw new \RuntimeException("Unexpected HTTP request. Invalid request.");
        }
    }
}

Der Client-Code für ein MVC-Typ-Setup in einem zentralen index.phpSystem sieht möglicherweise wie folgt aus (Überprüfungen werden weggelassen).

InboundHttpRequestFactory::setInjector($di);
InboundHttpRequestFactory::setMethod($httpRequestValidator->getMethod());
$di->set('InboundHttpRequest', InboundHttpRequestFactory::getRequest());
$router = $di->get('Router');  // The Router class depends on InboundHttpRequest objects.
$router->dispatch(); 

Alternativ können Sie die statische Natur (und das Schlüsselwort) der Factory entfernen und einem Abhängigkeitsinjektor erlauben, das Ganze zu verwalten (daher der auskommentierte Konstruktor). Sie müssen jedoch einige (nicht die Konstanten) der Klassenmitgliedsreferenzen ( self) in Instanzmitglieder ( $this) ändern .

Anthony Rutledge
quelle
Downvotes ohne Kommentare sind nicht sinnvoll.
Anthony Rutledge