Come gestisci le dipendenze che richiedono l'oggetto in cui ti inietti?

7

Attualmente sto cercando di ridefinire un grafico di oggetti piuttosto complicato per usare l'iniezione di dipendenza. Sto usando C # e StructureMap, ma il problema non è realmente specifico della tecnologia. Capisco il principio di base, ma non vedo come risolvere una dipendenza come questa:

public class Food
{    
    public Food(IIngredient ingredient)
    {       
        this.foodProcessor = new FoodProcessor(this);
    }

    // ... Lots of things to customize food

    public void BuildFood()
    {
        this.foodProcessor.Process();
    }
}

public class FoodProcessor
{
    public FoodProcessor(Food f)
    {
        this.food = f;
    }

    public void Process(){
        // yummy
    }
}

In tal caso, un FoodProcessor da solo non ha senso. Non riesco a elaborare null o cibo vuoto. D'altra parte, new nei vettori non è buono (giusto?), E dovrebbe essere iniettato come dipendenza.

In questo caso, la new è ok o è possibile effettuare il refactoring senza major modifiche alla mia applicazione?

    
posta Lennart 13.01.2017 - 14:41
fonte

4 risposte

15

Prendi ad esempio un Potato , che è probabilmente un tipo di Food . Generalmente, le patate non frustano un robot da cucina e si trasformano in torta del cottage quando le si mette accanto a carne macinata e carote. Eppure, probabilmente questo è ciò che l'esempio sopra indica. Quando il tuo modello oggetto suggerisce cose così sciocche, di solito è un segno che hai bisogno di un'astrazione diversa / migliore.

Mi piace che tu abbia scelto l'ingrediente. Questo ha sicuramente molto senso. Quello che non mi piace è che stai modellando il cibo come tutti i suoi ingredienti costitutivi, gli strumenti necessari per realizzarlo e il prodotto finale da consumare. Il cibo è responsabile di troppo, compresa la preparazione stessa, che è ciò che sta creando la dipendenza circolare. Inoltre, non ha senso avere un robot da cucina in Food. Dovrebbe probabilmente contenere gli ingredienti e trasformarli in cibo. Invece, lascia che ti proponga di dividere la tua classe di cibo per separare le sue attuali responsabilità.

public class Recipe
{
    private List<IIngredient> ingredients;
    private FoodProcessor foodProcessor;

    public Food prepare() {
        return this.foodProcessor.process(this.ingredients);
    }
}

Ora, con qualcosa di vagamente simile alla classe di cui sopra, abbiamo suddiviso una delle responsabilità della classe Food per interrompere quel ciclo di dipendenze. Abbiamo suddiviso il Cibo in due classi: Ricetta e Cibo. La ricetta rappresenta gli aspetti creativi del cibo, mentre il cibo è ora solo responsabile degli aspetti del "consumo" del cibo.

TLDR; Quando ti imbatti in cicli di dipendenza come questo, guarda il tuo modello di oggetti. In genere scoprirai che alcuni partecipanti sono responsabili di troppe cose, che in genere provengono dal modello a oggetti non essendo una rappresentazione sufficientemente accurata di ciò che stai modellando.

    
risposta data 13.01.2017 - 16:30
fonte
9

Mi sembra che tu abbia violato il principio della responsabilità unica. Se un cibo è davvero pensato per essere un contenitore per gli ingredienti, non ha alcun compito da sbrigare (abbina anche strettamente la tua classe di robot da cucina alla tua classe di cibo). Fare il cibo è il lavoro del robot da cucina.

Se un robot da cucina non può gestire un cibo vuoto o vuoto, rendilo esplicito. Fai un'eccezione se qualcuno cerca di usarlo in quel modo. Un'altra opzione consiste nel non consentire la creazione arbitraria di un robot da cucina e farlo passare attraverso un metodo di produzione che può imporre un contratto / rapporto complicato.

    
risposta data 13.01.2017 - 14:57
fonte
6

Per prima cosa, le altre risposte sono assolutamente corrette sul fatto che il tipo di dipendenza bidirezionale è solitamente un odore di codice, e se puoi refactarlo via, dovresti farlo.

Detto questo, se non si è in grado di effettuare il refactoring (ad esempio se così facendo richiederebbero modifiche su larga scala all'applicazione) StructureMap e altri contenitori DI spesso supportano Lazy Resolution .

Lazy<IFoodProcessor> foodProcessor;
public Food(IIngredient ingredient, Lazy<IFoodProcessor> foodProcessor) //or Func<IFoodProcessor>
{       
    this.foodProcessor = foodProcessor;
}
public void BuildFood()
{
    this.foodProcessor.Value.Process();
}

Ciò che viene rimandato è ottenere l'istanza foodProcessor effettiva fino a quando non viene utilizzata per la prima volta. E rendendo anche IFood per FoodProcessor Lazy , entrambi saranno costruibili dal tuo contenitore.

    
risposta data 13.01.2017 - 20:01
fonte
1

Quick aside: i concetti OOP sono spesso spiegati in termini di oggetti del mondo reale come automobili e forme, ma questo non è un ottimo modo per pensare quando si scrive codice orientato agli oggetti in quanto le cose tendono ad essere molto ondulate a mano. Pensa invece a ciò che il tuo programma deve effettivamente fare e a quali informazioni ha bisogno: questo è ciò che i tuoi oggetti dovrebbero rappresentare.

  • Il fatto che Food sia un sostantivo piuttosto che un verbo implica che Food è un insieme di proprietà che incapsula alcune informazioni sul cibo piuttosto che esistenti per svolgere qualche compito. Questo è rinforzato dal tuo commento "Un sacco di cose per personalizzare il cibo". Presumibilmente questo perché il tuo programma cattura queste informazioni in una parte del programma (ad esempio da un file o interfaccia utente) e usa queste informazioni altrove.

  • Il nome FoodProcessor insieme al metodo Process implica che questo oggetto incapsuli un comportamento o un'attività. Immagino che un vero robot da cucina taglierebbe Food in pezzi più piccoli, ma non ha senso io perché un programma per computer farebbe questo, quindi mi accingo a calcolare il costo degli ingredienti o qualcosa del genere.

Se FoodProcessor è in realtà una sorta di classe factory utilizzata per "creare" Food , la risposta di Dogs è azzeccata.

Ora sulla tua domanda:

  1. Come stai vedendo, vuoi evitare le dipendenze cicliche tra gli oggetti. Sicuramente vuoi evitare campi dipendenti ciclicamente (come nel tuo esempio), e a parità di altre cose, trovo meglio anche evitare le dipendenze cicliche del codice sorgente.

  2. Fintanto che FoodProcessor è responsabile dell'esecuzione di alcune attività su Food , è chiaro che l'oggetto processore ha bisogno di una dipendenza dall'oggetto che viene elaborato. L'oggetto in elaborazione tuttavia non ha realmente bisogno di una dipendenza dall'oggetto che lo elabora, il metodo BuildFood nel tuo esempio è in gran parte zucchero sintattico e potrebbe essere sostituito con un metodo di estensione.

  3. Quando si pensa a OOP in termini di oggetti del mondo reale si tenta di mettere tutte le cose che si possono "fare" con un oggetto come metodi su quell'oggetto (ad esempio si può preparare cibo, mangiare cibo, e-mail foto di cibo ecc ...). In pratica, questo tende a essere una cattiva idea in quanto significa che è necessario creare dipendenze per tutti quei processi al fine di creare un oggetto Food , anche se non si chiameranno tutti quei processi. Questo è un altro motivo per cui è meglio che un processo dipenda dall'oggetto che elabora e non viceversa.

  4. Per inciso, quando si ha una classe il cui compito è elaborare un oggetto, trovo il modo migliore per passare quell'oggetto come argomento al suo metodo di processo, piuttosto che come dipendenza del costruttore. Ciò consente alla stessa istanza FoodProcessor di elaborare più elementi.

Per riassumere, probabilmente eliminerei il metodo BuildFood e interromperò la dipendenza su FoodProcessor .

On the other hand, new in ctors are not good (right?)

new in un costruttore non è automaticamente negativo, ad es. una classe come DataTable creerà nuovi elenchi per le righe e le colonne. Il motivo per cui ciò va bene è che gli utenti della classe DataTable non vogliono essere in grado di sostituire la raccolta di righe con la propria.

Dovresti considerare se gli utenti della tua classe vorranno passare alla loro implementazione di sostituzione di quella dipendenza (non dimenticare i test unitari). Se non vuoi mai iniettare un'implementazione alternativa allora new di distanza (è davvero facile rifattorizzare un costruttore per accettare una dipendenza se ne trovi la necessità in futuro). Se il 99% delle volte si utilizza una dipendenza "predefinita", forse fare qualcosa del genere:

public class Foo
{
    private readonly IBar _bar;

    public Foo(IBar bar = null)
    {
        _bar = bar ?? new Bar();
    }
}
    
risposta data 17.01.2017 - 02:03
fonte

Leggi altre domande sui tag