Dovrei usare l'iniezione di dipendenza o fabbriche statiche?


81

Durante la progettazione di un sistema, mi trovo spesso ad affrontare il problema di avere un gruppo di moduli (registrazione, accesso al database, ecc.) Utilizzati dagli altri moduli. La domanda è: come posso fornire questi componenti ad altri componenti. Due risposte sembrano possibili iniezione di dipendenza o utilizzo del modello di fabbrica. Tuttavia entrambi sembrano sbagliati:

  • Le fabbriche rendono il test una seccatura e non consentono un facile scambio di implementazioni. Inoltre non rendono evidenti le dipendenze (ad esempio, stai esaminando un metodo, ignaro del fatto che chiama un metodo che chiama un metodo che chiama un metodo che utilizza un database).
  • L'iniezione di dipendenza gonfia enormemente gli elenchi di argomenti del costruttore e diffonde alcuni aspetti in tutto il codice. La situazione tipica è quella in cui i costruttori di oltre la metà delle classi sembrano così(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

Ecco una situazione tipica con cui ho un problema: ho classi di eccezioni, che usano descrizioni di errori caricate dal database, usando una query che ha un parametro di impostazione della lingua dell'utente, che si trova nell'oggetto sessione dell'utente. Quindi per creare una nuova eccezione ho bisogno di una descrizione, che richiede una sessione del database e la sessione dell'utente. Quindi sono condannato a trascinare tutti questi oggetti attraverso tutti i miei metodi nel caso in cui potessi aver bisogno di lanciare un'eccezione.

Come affrontare un problema del genere ??


2
Se una factory può risolvere tutti i tuoi problemi, forse potresti semplicemente iniettare la factory nei tuoi oggetti e ottenere LoggingProvider, DbSessionProvider, ExceptionFactory, UserSession.
Giorgio

1
Troppi "input" per un metodo, siano essi passati o iniettati, sono più un problema della progettazione stessa dei metodi. Qualunque cosa tu vada, potresti voler ridurre un po 'le dimensioni dei tuoi metodi (il che è più facile da fare una volta che hai inserito l'iniezione)
Bill K

La soluzione qui non dovrebbe ridurre gli argomenti. Costruisci invece astrazioni che costruiscono un oggetto di livello superiore che fa tutto il lavoro nell'oggetto e ti dà beneficio.
Alex,

Risposte:


74

Usa l'iniezione delle dipendenze, ma ogni volta che la lista degli argomenti del costruttore diventa troppo grande, esegui il refactoring utilizzando un servizio di facciata . L'idea è di raggruppare alcuni degli argomenti del costruttore, introducendo una nuova astrazione.

Ad esempio, è possibile introdurre un nuovo tipo che SessionEnvironmentincapsula a DBSessionProvider, il UserSessione il caricato Descriptions. Per sapere quali astrazioni hanno più senso, tuttavia, è necessario conoscere i dettagli del programma.

Una domanda simile è già stata posta qui su SO .


9
+1: Penso che raggruppare gli argomenti del costruttore in classi sia un'ottima idea. Ti costringe anche a organizzare questi argomenti in strutture più significative.
Giorgio

5
Ma se il risultato non è una struttura significativa, allora stai solo nascondendo la complessità che viola SRP. In questo caso dovrebbe essere eseguito un refactoring di classe.
danidacar,

1
@Giorgio Non sono d'accordo con un'affermazione generale secondo cui "raggruppare argomenti costruttivi in ​​classi è un'ottima idea". Se lo qualifichi con "in questo scenario", allora è diverso.
tymtam,

19

L'iniezione di dipendenza gonfia enormemente gli elenchi di argomenti del costruttore e diffonde alcuni aspetti in tutto il codice.

Da ciò, non sembra che tu capisca DI proprio - l'idea è di invertire il modello di istanza dell'oggetto all'interno di una fabbrica.

Il tuo problema specifico sembra essere un problema OOP più generale. Perché gli oggetti non possono semplicemente generare eccezioni normali e non leggibili dall'uomo durante il loro runtime e quindi avere qualcosa prima del tentativo / cattura finale che catturi quell'eccezione e a quel punto usa le informazioni della sessione per lanciare una nuova eccezione più carina ?

Un altro approccio sarebbe quello di avere una factory di eccezione, che viene passata agli oggetti attraverso i loro costruttori. Invece di lanciare una nuova eccezione, la classe può utilizzare un metodo di fabbrica (ad es throw PrettyExceptionFactory.createException(data).

Tieni presente che i tuoi oggetti, a parte quelli di fabbrica, non dovrebbero mai usare l' newoperatore. Le eccezioni sono generalmente l'unico caso speciale, ma nel tuo caso potrebbero essere un'eccezione!


1
Ho letto da qualche parte che quando l'elenco dei parametri diventa troppo lungo non è perché stai usando l'iniezione di dipendenza ma perché hai bisogno di più iniezione di dipendenza.
Giorgio

Questo potrebbe essere uno dei motivi - generalmente, a seconda della lingua, il costruttore non dovrebbe avere più di 6-8 argomenti e non più di 3-4 di questi dovrebbero essere oggetti stessi, a meno che il modello specifico (come il Buildermodello) lo impone. Se stai passando parametri al costruttore perché il tuo oggetto crea un'istanza di altri oggetti, questo è un caso chiaro per l'IoC.
Jonathan Rich

12

Hai già elencato abbastanza bene gli svantaggi del modello statico di fabbrica, ma non sono del tutto d'accordo con gli svantaggi del modello di iniezione di dipendenza:

L'iniezione di dipendenza richiede che tu scriva il codice per ogni dipendenza non è un bug, ma una caratteristica: ti costringe a pensare se hai davvero bisogno di queste dipendenze, promuovendo così l'accoppiamento libero. Nel tuo esempio:

Ecco una situazione tipica con cui ho un problema: ho classi di eccezioni, che usano descrizioni di errori caricate dal database, usando una query che ha un parametro di impostazione della lingua dell'utente, che si trova nell'oggetto sessione dell'utente. Quindi per creare una nuova eccezione ho bisogno di una descrizione, che richiede una sessione del database e la sessione dell'utente. Quindi sono condannato a trascinare tutti questi oggetti attraverso tutti i miei metodi nel caso in cui potessi aver bisogno di lanciare un'eccezione.

No, non sei condannato. Perché è responsabilità della logica aziendale localizzare i messaggi di errore per una determinata sessione utente? Che cosa succede se, in futuro, si desidera utilizzare quel servizio aziendale da un programma batch (che non ha una sessione utente ...)? O se il messaggio di errore non dovesse essere mostrato all'utente attualmente connesso, ma al suo supervisore (che potrebbe preferire una lingua diversa)? O se volessi riutilizzare la logica di business sul client (che non ha accesso a un database ...)?

Chiaramente, la localizzazione dei messaggi dipende da chi li guarda, cioè è responsabilità del livello di presentazione. Pertanto, genererei eccezioni ordinarie dal servizio aziendale, che si presentano con un identificatore di messaggio che può quindi essere cercato nel gestore delle eccezioni del livello di presentazione in qualunque sorgente di messaggi venga utilizzata.

In questo modo, puoi rimuovere 3 dipendenze non necessarie (UserSession, ExceptionFactory e probabilmente descrizioni), rendendo così il tuo codice sia più semplice che più versatile.

In generale, utilizzerei solo fabbriche statiche per le cose di cui hai bisogno di un accesso onnipresente e che sono garantite per essere disponibili in tutti gli ambienti in cui potremmo mai voler eseguire il codice (come la registrazione). Per tutto il resto, userei la semplice vecchia iniezione di dipendenza.


Questo. Non riesco a vedere la necessità di colpire il DB per generare un'eccezione.
Caleth,

1

Usa l'iniezione di dipendenza. L'uso di fabbriche statiche è un impiego Service Locatordell'antipasto. Guarda il lavoro fondamentale di Martin Fowler qui - http://martinfowler.com/articles/injection.html

Se i tuoi argomenti del costruttore diventano troppo grandi e non stai usando un contenitore DI, scrivi le tue fabbriche per l'istanza, permettendogli di essere configurabile, tramite XML o associando un'implementazione a un'interfaccia.


5
Service Locator non è un antipattern - Fowler stesso lo fa riferimento nell'URL che hai pubblicato. Mentre il modello di Service Locator può essere abusato (nello stesso modo in cui i Singleton vengono abusati - per sottrarre lo stato globale), è un modello molto utile.
Jonathan Rich,

1
Interessante da sapere. L'ho sempre sentito come anti schema.
Sam

4
È solo un antipasto se il localizzatore di servizi viene utilizzato per memorizzare lo stato globale. Il localizzatore di servizio dovrebbe essere un oggetto senza stato dopo l'istanza e preferibilmente immutabile.
Jonathan Rich

XML non è sicuro. Lo considererei un piano z dopo che ogni altro approccio ha fallito
Richard Tingle,

1

Vorrei andare anche con Iniezione delle dipendenze. Ricorda che DI non viene eseguito solo tramite i costruttori, ma anche tramite i setter Proprietà. Ad esempio, il logger potrebbe essere iniettato come proprietà.

Inoltre, potresti voler utilizzare un contenitore IoC che potrebbe sollevare parte dell'onere per te, ad esempio mantenendo i parametri del costruttore su cose che sono necessarie in fase di esecuzione dalla logica del tuo dominio (mantenendo il costruttore in un modo che rivela l'intenzione di la classe e le dipendenze del dominio reale) e magari iniettare altre classi helper attraverso le proprietà.

Un ulteriore passo che potresti voler fare è Programmnig orientato all'aspetto, che è implementato in molti framework principali. Ciò può consentire di intercettare (o "consigliare" di utilizzare la terminologia AspectJ) il costruttore della classe e iniettare le proprietà pertinenti, magari con un attributo speciale.


4
Evito DI attraverso setter perché introduce una finestra temporale durante la quale l'oggetto non è completamente inizializzato (tra costruttore e chiamata setter). O in altre parole introduce un ordine di chiamata del metodo (deve chiamare X prima di Y) che evito se possibile.
RokL,

1
DI attraverso i setter di proprietà è ideale per dipendenze opzionali. La registrazione è un buon esempio. Se è necessaria la registrazione, impostare la proprietà Logger, in caso contrario, non impostarla.
Preston,

1

Le fabbriche rendono il test una seccatura e non consentono un facile scambio di implementazioni. Inoltre non rendono evidenti le dipendenze (ad esempio, stai esaminando un metodo, ignaro del fatto che chiama un metodo che chiama un metodo che chiama un metodo che utilizza un database).

Non sono del tutto d'accordo. Almeno non in generale.

Semplice fabbrica:

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

Iniezione semplice:

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

Entrambi gli snippet hanno lo stesso scopo, creano un collegamento tra IFooe Foo. Tutto il resto è solo sintassi.

Il passaggio Fooa ADifferentFoorichiede esattamente lo stesso sforzo in entrambi i campioni di codice.
Ho sentito che le persone sostengono che l'iniezione di dipendenza consente di utilizzare diversi vincoli, ma lo stesso argomento può essere fatto sul fare fabbriche diverse. Scegliere la giusta rilegatura è esattamente complesso quanto scegliere la giusta fabbrica.

I metodi di fabbrica ti consentono, ad esempio, di utilizzarli Fooin alcuni luoghi e ADifferentFooin altri luoghi. Alcuni potrebbero definirlo buono (utile se ne hai bisogno), altri potrebbero chiamarlo cattivo (potresti fare un lavoro a metà per sostituire tutto).
Tuttavia, non è poi così difficile evitare questa ambiguità se ti attieni a un singolo metodo che ritorna in IFoomodo da avere sempre un'unica fonte. Se non vuoi spararti nel piede, non tenere una pistola carica o assicurarti di non mirare al piede.


L'iniezione di dipendenza gonfia enormemente gli elenchi di argomenti del costruttore e diffonde alcuni aspetti in tutto il codice.

Questo è il motivo per cui alcune persone preferiscono recuperare esplicitamente dipendenze nel costruttore, in questo modo:

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

Ho sentito argomenti pro (nessun costruttore gonfio), ho sentito argomenti con (l'uso del costruttore consente più automazione DI).

Personalmente, mentre ho ceduto al nostro anziano che vuole usare argomenti di costruzione, ho notato un problema con l'elenco a discesa in VS (in alto a destra, per sfogliare i metodi della classe corrente) in cui i nomi dei metodi spariscono quando uno delle firme del metodo è più lungo del mio schermo (=> il costruttore gonfio).

A livello tecnico, non mi interessa in alcun modo. Entrambe le opzioni richiedono lo stesso sforzo per digitare. E dal momento che stai usando DI, di solito non chiamerai un costruttore manualmente comunque. Ma il bug dell'interfaccia utente di Visual Studio mi fa preferire non gonfiare l'argomento del costruttore.


Come nota a margine, l' iniezione di dipendenza e le fabbriche non si escludono a vicenda . Ho avuto casi in cui invece di inserire una dipendenza, ho inserito una factory che genera dipendenze (NInject fortunatamente ti consente di utilizzare Func<IFoo>quindi non è necessario creare una classe factory effettiva).

I casi d'uso per questo sono rari ma esistono.


OP chiede delle fabbriche statiche . stackoverflow.com/questions/929021/...
Basilevs

@Basilevs "La domanda è: come posso fornire questi componenti ad altri componenti."
Anthony Rutledge,

1
@Basilevs Tutto ciò che ho detto, oltre alla nota a margine, si applica anche alle fabbriche statiche. Non sono sicuro di ciò che stai cercando di sottolineare in modo specifico. A cosa si riferisce il link?
Flater

Uno di questi casi d'uso dell'iniezione di una fabbrica potrebbe essere un caso in cui uno ha ideato una classe di richiesta HTTP astratta e ha strategicamente cinque altre classi figlio polimorfiche, una per GET, POST, PUT, PATCH e DELETE? Non è possibile sapere quale metodo HTTP verrà utilizzato ogni volta quando si tenta di integrare detta gerarchia di classi con un router di tipo MVC (che può dipendere in parte da una classe di richiesta HTTP. L'interfaccia dei messaggi HTTP PSR-7 è brutta.
Anthony Rutledge,

@AnthonyRutledge: le fabbriche DI possono significare due cose (1) Una classe che espone più metodi. Penso che sia di questo che stai parlando. Tuttavia, questo non è proprio particolare per le fabbriche. Che si tratti di una classe di logica aziendale con diversi metodi pubblici o di una fabbrica con diversi metodi pubblici, è una questione di semantica e non fa alcuna differenza tecnica. (2) Il caso d'uso specifico di DI per le fabbriche è che una dipendenza non di fabbrica viene istanziata una volta (durante l'iniezione), mentre una versione di fabbrica può essere utilizzata per istanziare la dipendenza effettiva in una fase successiva (e possibilmente più volte).
Flater,

0

In questo esempio fittizio, una runtime viene utilizzata in fase di esecuzione per determinare quale tipo di oggetto richiesta HTTP in entrata da istanziare, in base al metodo di richiesta HTTP. La fabbrica stessa viene iniettata con un'istanza di un contenitore di iniezione di dipendenza. Ciò consente alla fabbrica di determinare la sua autonomia e lasciare che il contenitore di iniezione delle dipendenze gestisca le dipendenze. Ogni oggetto richiesta HTTP in entrata ha almeno quattro dipendenze (superglobali e altri oggetti).

<?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.");
        }
    }
}

Il codice client per una configurazione di tipo MVC, all'interno di una centralizzata index.php, potrebbe apparire come segue (convalide omesse).

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(); 

In alternativa, è possibile rimuovere la natura statica (e la parola chiave) della factory e consentire a un iniettore di dipendenze di gestire l'intera cosa (quindi, il costruttore commentato). Tuttavia, dovrai cambiare alcune (non le costanti) dei riferimenti dei membri della classe ( self) in membri dell'istanza ( $this).


I downgrade senza commenti non sono utili.
Anthony Rutledge,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.