Esiste un modello per l'inizializzazione di oggetti creati tramite un contenitore DI


147

Sto cercando di ottenere Unity per gestire la creazione dei miei oggetti e voglio avere alcuni parametri di inizializzazione che non sono noti fino al runtime:

Al momento l'unico modo in cui ho potuto pensare al modo di farlo è avere un metodo Init sull'interfaccia.

interface IMyIntf {
  void Initialize(string runTimeParam);
  string RunTimeParam { get; }
}

Quindi per usarlo (in Unity) farei questo:

var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");

In questo scenario, il runTimeParamparametro viene determinato in fase di esecuzione in base all'input dell'utente. Il banale caso qui restituisce semplicemente il valore di runTimeParamma in realtà il parametro sarà qualcosa come il nome del file e il metodo di inizializzazione farà qualcosa con il file.

Ciò crea una serie di problemi, vale a dire che il Initializemetodo è disponibile sull'interfaccia e può essere chiamato più volte. Impostare un flag nell'implementazione e lanciare un'eccezione su una chiamata ripetuta Initializesembra molto goffo.

Nel momento in cui risolvo la mia interfaccia, non voglio sapere nulla sull'implementazione di IMyIntf. Quello che voglio, tuttavia, è la conoscenza che questa interfaccia necessita di determinati parametri di inizializzazione una volta. C'è un modo per annotare in qualche modo (attributi?) L'interfaccia con queste informazioni e passarle al framework quando viene creato l'oggetto?

Modifica: ha descritto l'interfaccia un po 'di più.


9
Ti manca il punto di usare un contenitore DI. Le dipendenze dovrebbero essere risolte per te.
Pierreten,

Da dove stai ottenendo i parametri necessari? (file di configurazione, db, ??)
Jaime

runTimeParamè una dipendenza determinata in fase di esecuzione in base a un input dell'utente. L'alternativa dovrebbe essere quella di suddividerla in due interfacce: una per l'inizializzazione e l'altra per la memorizzazione dei valori?
Igor Zevaka,

dipendenza in IoC, di solito si riferisce alla dipendenza da altre classi o oggetti di tipo ref che possono essere determinati in fase di inizializzazione IoC. Se la tua classe ha bisogno solo di alcuni valori per funzionare, è qui che diventa utile il metodo Initialize () nella tua classe.
The Light

Voglio dire, ci sono 100 classi nella tua app su cui questo approccio può essere applicato; quindi dovrai creare altre 100 classi di fabbrica + 100 interfacce per le tue classi e potresti cavartela se stessi usando il metodo Initialize ().
The Light

Risposte:


276

In qualsiasi luogo in cui è necessario un valore di runtime per costruire una particolare dipendenza, Abstract Factory è la soluzione.

Avere i metodi Initialize sulle interfacce ha un odore di Leaky Abstraction .

Nel tuo caso, direi che dovresti modellare l' IMyIntfinterfaccia su come devi usarla, non su come intendi crearne implementazioni. Questo è un dettaglio di implementazione.

Pertanto, l'interfaccia dovrebbe essere semplicemente:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Ora definisci la Fabbrica astratta:

public interface IMyIntfFactory
{
    IMyIntf Create(string runTimeParam);
}

Ora puoi creare un'implementazione concreta IMyIntfFactoryche crea istanze concrete IMyIntfcome questa:

public class MyIntf : IMyIntf
{
    private readonly string runTimeParam;

    public MyIntf(string runTimeParam)
    {
        if(runTimeParam == null)
        {
            throw new ArgumentNullException("runTimeParam");
        }

        this.runTimeParam = runTimeParam;
    }

    public string RunTimeParam
    {
        get { return this.runTimeParam; }
    }
}

Nota come questo ci consente di proteggere gli invarianti della classe usando la readonlyparola chiave. Non sono necessari metodi di inizializzazione puzzolenti.

Un IMyIntfFactoryattuazione può essere semplice come questo:

public class MyIntfFactory : IMyIntfFactory
{
    public IMyIntf Create(string runTimeParam)
    {
        return new MyIntf(runTimeParam);
    }
}

In tutti i tuoi consumatori in cui hai bisogno di IMyIntfun'istanza, prendi semplicemente una dipendenza IMyIntfFactoryrichiedendola tramite Iniezione costruttore .

Qualsiasi contenitore DI degno del suo sale sarà in grado di cablare automaticamente IMyIntfFactoryun'istanza per te se la registri correttamente.


13
Il problema è che un metodo (come Initialize) fa parte dell'API, mentre il costruttore non lo è. blog.ploeh.dk/2011/02/28/InterfacesAreAccessModifiers.aspx
Mark Seemann

13
Inoltre, un metodo Initialize indica l'accoppiamento temporale: blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx
Mark Seemann,

2
@Darlene Potresti essere in grado di utilizzare un Decorator pigramente inizializzato, come descritto nella sezione 8.3.6 del mio libro . Fornisco anche un esempio di qualcosa di simile nella mia presentazione Big Object Graphs Up Front .
Mark Seemann,

2
@Mark Se la creazione dell'implementazione della factory MyIntfrichiede più di runTimeParam(leggi: altri servizi che si vorrebbe risolvere da un IoC), allora si è ancora a dover risolvere quelle dipendenze nella propria factory. Mi piace la risposta di @PhilSandler di passare quelle dipendenze nel costruttore della fabbrica per risolvere questo problema: anche tu la prendi in considerazione?
Jeff

2
Anche cose fantastiche, ma la tua risposta a questa altra domanda è arrivata davvero al mio punto.
Jeff

15

Di solito quando si verifica questa situazione, è necessario rivisitare il progetto e determinare se si stanno mescolando gli oggetti stateful / data con i servizi puri. Nella maggior parte (non in tutti) i casi, dovrai tenere separati questi due tipi di oggetti.

Se hai bisogno di un parametro specifico del contesto passato nel costruttore, un'opzione è quella di creare un factory che risolva le dipendenze del tuo servizio tramite il costruttore e accetta il tuo parametro di runtime come parametro del metodo Create () (o Genera ( ), Build () o qualunque sia il nome dei metodi di fabbrica).

Avere setter o un metodo Initialize () lo sono generalmente considerato una cattiva progettazione, in quanto è necessario "ricordare" di chiamarli e assicurarsi che non aprano troppo lo stato dell'implementazione (cioè cosa impedisce a qualcuno di ri -calling inizializzazione o setter?).


5

Mi sono anche imbattuto in questa situazione alcune volte in ambienti in cui sto creando dinamicamente oggetti ViewModel basati su oggetti Model (delineati molto bene da questo altro post Stackoverflow ).

Mi è piaciuto come l' estensione Ninject che ti consente di creare dinamicamente fabbriche basate su interfacce:

Bind<IMyFactory>().ToFactory();

Non sono riuscito a trovare alcuna funzionalità simile direttamente in Unity ; così ho scritto la mia estensione su IUnityContainer che ti consente di registrare fabbriche che creeranno nuovi oggetti basati su dati da oggetti esistenti essenzialmente mappando da una gerarchia di tipi a una gerarchia di tipi diversi: UnityMappingFactory @ GitHub

Con un obiettivo di semplicità e leggibilità, ho finito con un'estensione che consente di specificare direttamente i mapping senza dichiarare le singole classi o interfacce di fabbrica (un risparmio di tempo reale). Devi solo aggiungere i mapping proprio dove registri le classi durante il normale processo di bootstrap ...

//make sure to register the output...
container.RegisterType<IImageWidgetViewModel, ImageWidgetViewModel>();
container.RegisterType<ITextWidgetViewModel, TextWidgetViewModel>();

//define the mapping between different class hierarchies...
container.RegisterFactory<IWidget, IWidgetViewModel>()
.AddMap<IImageWidget, IImageWidgetViewModel>()
.AddMap<ITextWidget, ITextWidgetViewModel>();

Quindi devi semplicemente dichiarare l'interfaccia della factory di mappatura nel costruttore per CI e usare il suo metodo Create () ...

public ImageWidgetViewModel(IImageWidget widget, IAnotherDependency d) { }

public TextWidgetViewModel(ITextWidget widget) { }

public ContainerViewModel(object data, IFactory<IWidget, IWidgetViewModel> factory)
{
    IList<IWidgetViewModel> children = new List<IWidgetViewModel>();
    foreach (IWidget w in data.Widgets)
        children.Add(factory.Create(w));
}

Come bonus aggiuntivo, anche eventuali dipendenze aggiuntive nel costruttore delle classi mappate verranno risolte durante la creazione dell'oggetto.

Ovviamente, questo non risolverà tutti i problemi, ma finora mi è servito abbastanza bene, quindi ho pensato di condividerlo. C'è più documentazione sul sito del progetto su GitHub.


1

Non posso rispondere con una terminologia specifica di Unity ma sembra che tu stia solo imparando l'iniezione di dipendenza. In tal caso, ti esorto a leggere la guida per l'utente breve, chiara e ricca di informazioni per Ninject .

Questo ti guiderà attraverso le varie opzioni che hai quando usi DI e come spiegare i problemi specifici che dovrai affrontare lungo il percorso. Nel tuo caso, molto probabilmente vorrai usare il contenitore DI per creare un'istanza dei tuoi oggetti e far sì che quell'oggetto ottenga un riferimento a ciascuna delle sue dipendenze attraverso il costruttore.

La procedura dettagliata spiega inoltre come annotare metodi, proprietà e persino parametri utilizzando gli attributi per distinguerli in fase di esecuzione.

Anche se non usi Ninject, la procedura dettagliata ti fornirà i concetti e la terminologia della funzionalità adatta al tuo scopo e dovresti essere in grado di mappare tale conoscenza su Unity o altri framework DI (o convincerti a provare Ninject) .


Grazie per quello. Sto attualmente valutando i framework DI e NInject sarebbe stato il mio prossimo.
Igor Zevaka,


1

Penso di averlo risolto e mi sembra abbastanza salutare, quindi deve essere per metà giusto :))

Mi sono diviso IMyIntfin interfacce "getter" e "setter". Così:

interface IMyIntf {
  string RunTimeParam { get; }
}


interface IMyIntfSetter {
  void Initialize(string runTimeParam);
  IMyIntf MyIntf {get; }
}

Quindi l'implementazione:

class MyIntfImpl : IMyIntf, IMyIntfSetter {
  string _runTimeParam;

  void Initialize(string runTimeParam) {
    _runTimeParam = runTimeParam;
  }

  string RunTimeParam { get; }

  IMyIntf MyIntf {get {return this;} }
}

//Unity configuration:
//Only the setter is mapped to the implementation.
container.RegisterType<IMyIntfSetter, MyIntfImpl>();
//To retrieve an instance of IMyIntf:
//1. create the setter
IMyIntfSetter setter = container.Resolve<IMyIntfSetter>();
//2. Init it
setter.Initialize("someparam");
//3. Use the IMyIntf accessor
IMyIntf intf = setter.MyIntf;

IMyIntfSetter.Initialize()può ancora essere chiamato più volte, ma usando bit di paradigma di Service Locator possiamo concludere abbastanza bene che IMyIntfSetterè quasi un'interfaccia interna a cui si distingue IMyIntf.


13
Questa non è una soluzione particolarmente buona poiché si basa su un metodo Initialize, che è un'Astrazione che perde. A proposito, questo non sembra Service Locator, ma più come Interface Injection. In ogni caso, vedi la mia risposta per una soluzione migliore.
Mark Seemann,
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.