Come impedire l'iniezione delle dipendenze dall'uccidere la programmazione orientata agli oggetti

4

Recentemente molte persone hanno adottato DI nei loro progetti (sto lavorando con il nucleo di aspnet). Il problema che ho è che DI trasforma il mio codice verso il paradigma procedurale. Ad esempio in modo OOP vorrei fare:

class Something
{
    Something(Other object, Dependency dep); // created manually or by factory obj

    DoStuff();
}

Tuttavia con DI non posso creare direttamente la classe Something, perché ho bisogno di soddisfare le dipendenze. Ora il mio codice è generalmente simile a questo:

class Something
{
    Something(Dependency dep); // resolved from di container

    DoStuff(Object obj); // executed manually
}

Di conseguenza i miei oggetti non hanno metodi, sono solo pacchetti di proprietà che vengono elaborati da diversi servizi. D'altra parte dovrei creare oggetti giganti con molte dipendenze solo per eseguire compiti semplici, che richiedono l'1% del grafico dell'oggetto creato.

Come risolvere il mio progetto? Questo approccio ha svantaggi evidenti, ad esempio non posso passare a Qualcosa in classe, perché ha bisogno di Object to DoStuff, quindi devo inserirlo in qualche altra classe. Ci sono molti altri problemi, come la funzionalità diffusa su molti file, le difficoltà nell'implementazione della sicurezza (perché è possibile richiedere il servizio "interno" da DI) o l'incapsulamento (l'oggetto deve avere tutte le proprietà pubbliche perché sono manipolate dalla classe esterna.

Modifica: Piccolo esempio per una migliore illustrazione:

Api scriverei prima di DI:

class ShoppingCartFactory {
    ShoppingCart Create(id);
}

// encapsulates cart logic
class ShoppingCart {
    // internal dependencies passed to cart from factory
    ShoppingCart(DeliveryLoader, ItemMetadataLaoder);


    // methods encapsulation functionality
    Modify(item, quantity);
    Save();
    Empty();
    GetDeliveryOptions(); // loads possible transports from db
    LoadItemMetadata(); // loads prices, availability etc. for items

    // public properties
    ImmutableList<CartItem> items; // immutable list of items
    Delivery selectedDelivery; // transport selected by client
}

Quando il cliente fa clic sulle opzioni di consegna del carrello acquisti vengono visualizzate solo una volta e le richieste successive non ne hanno bisogno. Pertanto con questo approccio devo creare la dipendenza ShoppingCartDeliveryCreator per ogni richiesta.

Quindi, invece, devo scrivere qualcosa del genere:

// instead of having properties on cart Object now I have 3 separate classes
// in previous example user of api had no idea how cart works inside
// now he needs to find all classes associated with cart
// also service class needs to take id in each method
// or needs some method like 'Contextualize(id)'
class ShoppingCartService {
    // executes logic on db
    ShoppingCart Read(id);
    ShoppingCart Modify(id, item, quenatity);
    ShoppingCart Empty(id);
    ShoppingCart Save(id, shoppingCart);
}
class ShoppingCartDeliveryCreator (...)
class ShoppingCartItemMetadataLoader (...)

// no logic
class ShoppingCart {
    List<CartItem> items; // cannot be immutable, because Service needs to manipulate it
    Delivery selectedDelivery; // same
}

Non puoi fare nulla con la classe ShoppingCart, hai sempre bisogno di uno dei servizi. Anche come ha sottolineato Derek Elkins

One of the consequences of systematically using Dependency Injection is that your class hierarchy becomes completely flat.

Non sono sicuro che questa sia una buona cosa. Ho fatto molte ricerche ed esperimenti, tuttavia non sono convinto di questo stile di programmazione. Per avere un'idea di come funziona qualcosa Devi esaminare tutto quel codice, indovinare cosa sta facendo e quindi potresti avere un'idea sottile di come funziona. Con il vecchio esempio potevi semplicemente guardare sulla classe e vedresti tutte le opzioni, oppure puoi controllare tutte le classi che prendono ShoppingCart.

    
posta Shadow 17.05.2017 - 09:44
fonte

3 risposte

13

Come accennato da Doc Brown, l'uso dei contenitori di Dipendenza per iniezione non è Iniezione delle dipendenze. L'iniezione di dipendenza è solo parametrizzazione, ei contenitori di iniezione di dipendenza servono solo a "semplificare" le connessioni di cablaggio . Il tuo problema sembra essere principalmente guidato dai (percepiti) limiti di qualsiasi framework di container DI che stai utilizzando.

Non capisco come i tuoi oggetti siano "sacchi di proprietà" è un "risultato" dell'iniezione di dipendenza. Alcuni oggetti sono diventeranno semplicemente "sacchi di proprietà" e quindi non avranno alcuna dipendenza. Questi sono spesso chiamati oggetti "POJO / POCO" per "Plain Old Java / C # Objects". Ovviamente, questo non può essere tutti i tuoi oggetti in quanto nulla avrebbe bisogno di dipendenze allora.

Hai anche menzionato "oggetti giganti con molte dipendenze" che "richiede [solo] l'1% di [il] oggetto grafico creato". Questo è in qualche modo contraddittorio. Se ha solo bisogno dell'1% del "grafico dell'oggetto creato", perché dipende da quel restante 99%? Se le tue dipendenze hanno bisogno di dipendenze per cose diverse da quelle che ti servono delle dipendenze, questo suggerisce che le tue dipendenze non sono ben calcolate. In termini OOD, una violazione del principio di responsabilità unica. Una delle conseguenze dell'uso sistematico di Iniezione delle dipendenze è che la gerarchia delle classi diventa completamente piatta. Ogni "gerarchia" necessaria nasce da come le cose sono collegate insieme (che non ha più bisogno di essere strettamente gerarchiche). Ciascuna delle tue classi dovrebbe dipendere solo da ciò di cui ha bisogno tramite interfacce relativamente strette. Se ogni classe fornisce solo un "servizio", le dipendenze non istanziano più del grafico dell'oggetto di quanto ne abbiano bisogno. Ci sono molte buone ragioni per cui la classe fornisce più "servizi", e questo può portare a dipendenze che sono estranee a qualche consumatore a valle del "servizio", ma questo sarà molto ovvio e presumibilmente fatto per qualche ragione esplicita. Altrimenti, considera la classe in più classi che forniscono ciascuno solo uno dei "servizi". (I "servizi" di solito corrispondono abbastanza strettamente alle interfacce di un'implementazione di classe, quindi se hai una classe che implementa più interfacce, questa è un'indicazione che la classe fornisce più "servizi".)

Ad ogni modo, per l'esempio limitato di codice che hai fornito, c'è una soluzione abbastanza chiara. In primo luogo, è del tutto possibile che qualunque framework di contenitore DIx tu stia usando esplicitamente, ma supponiamo di no. Di cosa hanno effettivamente bisogno i consumatori di Something ? La necessità di qualcosa che darà loro Something s quando viene dato Other s. (O per essere più precisi, qualcosa che fornirà loro istanze di un'interfaccia che rappresenta qualunque "servizio" Something s. Non dovrebbero importare se sono effettivamente Something s.). Cioè, hanno bisogno di un SomethingFactory . Quindi le classi che hanno bisogno di Something s dovrebbero dipendere da SomethingFactory e quindi usarle per creare Something s. Nel codice:

class Something : ISomething {
    public Something(Other object, Dependencies dependencies) {...}
    // ...
}

interface ISomethingFactory {
    ISomething CreateSomething(Other object);
}

class SomethingFactory : ISomethingFactory {
    private readonly Dependencies _dependencies;
    public SomethingFactory(Dependencies dependencies) {
        _dependencies = dependencies;
    }
    public ISomething CreateSomething(Other object) {
        return new Something(object, _dependencies);
    }
}

class SomethingConsumer {
    public SomethingConsumer(ISomethingFactory somethingFactory) {...}
}

Questo approccio ti dà quasi esattamente la stessa esperienza di scrivere direttamente new Something(object) plus non dovendo preoccuparti delle dipendenze estranee. Non vi è alcun motivo per quello che hai descritto (o in generale) perché la Dipendenza dall'iniezione ti porterebbe a dover rendere pubbliche tutte le proprietà o usare uno "stile procedurale" o qualcosa del genere. Si potrebbe prendere qualsiasi programma OO scritto "naturalmente" e convertirlo meccanicamente in uno usando l'iniezione di dipendenza. Passa semplicemente a quello che vorresti scrivere "naturalmente" in stile "OO" e sostituisci qualsiasi utilizzo di new con le dipendenze o le fabbriche passate come ho illustrato sopra. Oltre a dover passare alcuni parametri aggiuntivi, la struttura del codice sarà praticamente identica a quella di prima. Se ritieni di dover apportare cambiamenti molto più ampi di questo, stai fraintendendo qualcosa. (Questo potrebbe suggerire modifiche più estese, ma non dovrebbero essere necessarie.)

Come nota, il fatto che tu stia usando un particolare contenitore DI dovrebbe non essere evidente dalla maggior parte del tuo codice. Dovresti non passare intorno al contenitore DI e creare istanze con esso, volenti o nolenti. Il contenitore DI deve essere utilizzato (se utilizzato) per semplificare il codice di livello superiore che collega tutto insieme. Non sono sicuro di cosa intendi con "puoi richiedere il servizio 'interiore' da DI", ma sembra che tu stia facendo qualcosa del genere.

    
risposta data 17.05.2017 - 10:51
fonte
0

The problem I have is that DI turns my code towards procedural paradigm.

Il tuo esempio non mostra un "approccio procedurale".

As a result my objects have no methods, they are just bags of properties that are processed by different services. On the other hand I would have to create giant objects with lots of dependencies just to execute simple task, that needs 1% of created object graph.

How to fix my design?

forse lo schema livello singolo di astrazione aiuta qui:

Di solito applichiamo il modello SLA ai metodi, ovvero eseguono calcoli effettivi e / o cambiano lo stato degli oggetti o chiamano altri metodi (sulle dipendenze o nella stessa classe).

Ma possiamo estendere anche questo patter per le classi:

Dividi le tue classi in "worker" e "dispatcher" in modo da finire con classi che non hanno dipendenze da iniettare ma calcoli sui loro parametri e / o membri (i "worker" e altre classi che hanno dipendenze ma non calcolare nulla, basta chiamare metodi sulle sue dipendenze ( branching e looping permesso - i "dispatcher")

    
risposta data 17.05.2017 - 10:13
fonte
0

Stai confondendo i framework dei contenitori DI e DI

Ovviamente puoi creare manualmente il tuo oggetto. Passi solo nelle dipendenze di cui ha bisogno. Non devi sempre popolarli automaticamente da un contenitore.

public class myController
{
    IDependency dep;
    public myController(IDependency dep) // injected by di container
    {
        this.dep = dep;
    }
    public void Whatever()
    {
        var obj = new Object();
        var s = new Something(obj, dep); // injected manually
        s.DoSomething();
    }
}
    
risposta data 17.05.2017 - 10:57
fonte

Leggi altre domande sui tag