Devo usare Iniezione delle dipendenze o fabbriche statiche?

77

Durante la progettazione di un sistema mi trovo spesso ad affrontare il problema di avere un mucchio di moduli (logging, accesso ai database, ecc.) utilizzati dagli altri moduli. La domanda è, come faccio a fornire questi componenti ad altri componenti. Due risposte sembrano possibili iniezioni di dipendenza o utilizzando il modello di fabbrica. Comunque entrambi sembrano sbagliati:

  • Le fabbriche fanno provare un dolore e non consentono un facile scambio di implementazioni. Inoltre, non rendono le dipendenze apparenti (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 dipendenze fa gonfiare in modo massiccio gli elenchi degli argomenti del costruttore e mostra alcuni aspetti in tutto il codice. La situazione tipica è dove i costruttori di più della metà delle classi sembrano questo (....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

Ecco una situazione tipica con cui ho un problema: Ho delle classi di eccezioni, che usano le descrizioni degli errori caricate dal database, usando una query che ha un parametro di impostazione della lingua dell'utente, che è nell'oggetto della sessione utente. Quindi per creare una nuova eccezione ho bisogno di una descrizione, che richiede una sessione di database e la sessione dell'utente. Quindi sono condannato a trascinare tutti questi oggetti su tutti i miei metodi solo nel caso in cui possa essere necessario lanciare un'eccezione.

Come posso affrontare questo problema ??

    
posta U Mad 11.03.2013 - 13:36
fonte

7 risposte

72

Usa l'iniezione delle dipendenze, ma ogni volta che gli elenchi degli argomenti del costruttore diventano troppo grandi, rifattalo usando una Facciata Servizio . L'idea è di raggruppare insieme alcuni degli argomenti del costruttore, introducendo una nuova astrazione.

Ad esempio, potresti introdurre un nuovo tipo SessionEnvironment che incapsula un DBSessionProvider , il UserSession e il% co_de caricato. Per sapere quali astrazioni hanno più senso, tuttavia, è necessario conoscere i dettagli del programma.

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

    
risposta data 11.03.2013 - 13:57
fonte
19

Dependecy injection massively swells constructor argument lists and it smears some aspects all over your code.

Da ciò, non sembra che tu abbia capito bene DI - l'idea è di invertire il modello di istanziazione degli oggetti all'interno di una fabbrica.

Il tuo problema specifico sembra essere un problema di OOP più generale. Perché gli oggetti non possono solo generare eccezioni normali non leggibili durante il loro runtime e quindi avere qualcosa prima del try / catch finale che cattura quell'eccezione, e a quel punto usa le informazioni sulla sessione per lanciare una nuova, più bella eccezione ?

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

Ricorda che i tuoi oggetti, a parte gli oggetti di fabbrica, non dovrebbero mai usare l'operatore new . Le eccezioni sono generalmente un caso speciale, ma nel tuo caso potrebbero essere un'eccezione!

    
risposta data 11.03.2013 - 14:30
fonte
12

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

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 veramente bisogno di queste dipendenze, promuovendo così un accoppiamento libero. Nel tuo esempio:

Here's a typical situation I have a problem with: I have exception classes, which use error descriptions loaded from the database, using a query which has parameter of user language setting, which is in user session object. So to create a new Exception I need a description, which requires a database session and the user session. So I'm doomed to dragging all these objects across all my methods just in case I might need to throw an exception.

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

Chiaramente, la localizzazione dei messaggi dipende da chi guarda questi messaggi, cioè è responsabilità del livello di presentazione. Pertanto, lancerei ordinarie eccezioni dal servizio aziendale, che capita di portare un identificativo del messaggio che può quindi essere cercato sul gestore delle eccezioni del livello di presentazione in qualsiasi fonte di messaggi che capita di utilizzare.

In tal 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 le fabbriche statiche per le cose alle quali hai bisogno di accesso ubiquo e che sono garantite per essere disponibili in tutti gli ambienti in cui potremmo voler eseguire il codice (come il Logging). Per tutto il resto, userei la semplice vecchia iniezione di dipendenza.

    
risposta data 11.03.2013 - 23:34
fonte
1

Usa l'iniezione di dipendenza. L'uso di fabbriche statiche è un impiego del Service Locator antipattern. Vedi il lavoro fondamentale di Martin Fowler qui - link

Se gli argomenti del tuo costruttore diventano troppo grandi e non stai utilizzando un contenitore DI, scrivi tutte le tue fabbriche per l'istanziazione, consentendoti di configurarlo, tramite XML o collegando un'implementazione a un'interfaccia.

    
risposta data 11.03.2013 - 14:15
fonte
1

Anch'io vado bene con Dependency Injection. Ricorda che il DI non è fatto solo attraverso i costruttori, ma anche attraverso i setter di proprietà. Ad esempio, il logger potrebbe essere iniettato come una proprietà.

Inoltre, potresti voler utilizzare un contenitore IoC che potrebbe sollevare un po 'di peso per te, ad esempio mantenendo i parametri del costruttore su cose che sono necessarie in fase di esecuzione dalla tua logica di dominio (mantenendo il costruttore in un modo che rivela l'intenzione della classe e delle dipendenze dei domini reali) e magari iniettare altre classi di helper attraverso le proprietà.

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

    
risposta data 11.03.2013 - 14:42
fonte
0

Factories make testing a pain and don't allow easy swapping of implementations. They also don't make dependencies apparent (e.g. you're examining a method, oblivious to the fact that it calls a method that calls a method that calls a method that uses a database).

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

Fabbrica semplice:

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

Iniezione semplice:

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

Entrambi gli snippet hanno lo stesso scopo, impostano un collegamento tra IFoo e Foo . Tutto il resto è solo sintassi.

Cambiare Foo in ADifferentFoo richiede esattamente lo stesso sforzo in entrambi gli esempi di codice.
Ho sentito che le persone sostengono che l'iniezione di dipendenza consente l'uso di diversi binding, ma lo stesso argomento può essere fatto per creare diverse fabbriche. Scegliere l'associazione giusta è altrettanto complesso che scegliere la fabbrica giusta.

I metodi di produzione ti consentono, ad es. usa Foo in alcuni posti e ADifferentFoo in altri posti. Alcuni potrebbero chiamarlo buono (utile se ne hai bisogno), alcuni potrebbero chiamarlo così male (potresti fare un lavoro a metà asso nella sostituzione di tutto).
Tuttavia, non è poi così difficile evitare questa ambiguità se ci si attiene a un singolo metodo che restituisce IFoo in modo da avere sempre un'unica fonte. Se non vuoi spararti ai piedi, o non tenere una pistola carica, o assicurati di non mirare al tuo piede.

Dependecy injection massively swells constructor argument lists and it smears some aspects all over your code.

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

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

Ho sentito argomenti pro (nessun costruttore gonfia), ho sentito argomenti con (usando il costruttore abilita più automazione DI).

Personalmente, mentre ho ceduto al nostro senior che vuole usare gli argomenti del costruttore, ho notato un problema con il dropdownlist in VS (in alto a destra, per sfogliare i metodi della classe corrente) dove i nomi dei metodi escono da vista quando una delle firme del metodo è più lunga del mio schermo (= > il costruttore gonfiato).

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, in genere non chiamerai comunque un costruttore. Ma il bug dell'interfaccia utente di Visual Studio mi fa preferire non gonfiore dell'argomento costruttore.

Come nota a margine, l'iniezione delle dipendenze e le fabbriche non si escludono a vicenda . Ho avuto casi in cui invece di inserire una dipendenza, ho inserito uno stabilimento che genera dipendenze (NInject per fortuna consente di utilizzare Func<IFoo> in modo da non dover creare una classe factory effettiva).

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

    
risposta data 31.07.2018 - 14:41
fonte
0

In questo mock esempio, una classe factory viene utilizzata in fase di esecuzione per determinare quale tipo di oggetto HTTP in ingresso richiedere per istanziare, in base al metodo di richiesta HTTP. La fabbrica stessa viene iniettata con un'istanza di un contenitore di iniezione delle dipendenze. Ciò consente alla factory di eseguire la sua determinazione in fase di esecuzione e lasciare che il contenitore di iniezione delle dipendenze gestisca le dipendenze. Ogni oggetto di 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 un'impostazione di tipo MVC, all'interno di un index.php centralizzato, potrebbe essere simile al seguente (convalida omessa).

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 parola chiave) della factory e consentire a un injector delle dipendenze di gestire l'intera cosa (quindi, il costruttore commentato). Tuttavia, dovrai modificare alcuni (non le costanti) dei riferimenti dei membri della classe ( self ) ai membri dell'istanza ( $this ).

    
risposta data 31.07.2018 - 14:20
fonte

Leggi altre domande sui tag