Cosa mi manca nella mia (errata) comprensione di IoC / DI / Castle Windsor?

6

Ok, ecco come ho capito che IoC e DI in Web API funzionano quando uso Castle Windsor.

Nota, però, che la mia fiducia nel fatto che io la capisca come dovrei, comunque, cade da qualche parte tra la mia sicurezza che potrei meglio Dennis Rodman in una partita di basket uno contro uno e la mia fiducia nella sua sanità mentale.

Un altro modo per dirlo è che ho passato gli ultimi giorni a leggere su IoC / DI / Castle Windsor con Web API e a sperimentarlo, ma mi sento ancora stanco di rispondere, "Tutto tranne nove, pard; sistemali nell'altro vicolo. "

Ho diverse domande più specifiche su Stack Overflow, ad esempio questo , ma non sono ancora molto più chiaro a riguardo.

Quindi ho intenzione di "parlare ad alta voce" e spero che qualcuno risponda con qualcosa che raffini la mia comprensione, faccia luce su questo argomento opaco (per me), o aiuti a svelare questo nodo gordiano.

Punto: La base di IoC / DI consiste nel passare le interfacce ai costruttori, in modo che la classe con il costruttore non debba istanziare i propri oggetti; in effetti, non ha bisogno di conoscere la componente / classe concreta.

Punto: per configurarlo, hai bisogno di un codice come questo nei tuoi Controller:

private readonly IDepartmentRepository _deptsRepository;

public DepartmentsController(IDepartmentRepository deptsRepository)
{
    if (deptsRepository == null)
    {
        throw new ArgumentNullException("deptsRepository is null");
    }
    _deptsRepository = deptsRepository;
}

Punto: alcune classi che implementano IDepartmentRepository vengono passate al costruttore DepartmentsController

Punto: il codificatore non lo fa esplicitamente - il contenitore IOC (Castle Windsor, in questo caso) lo fa "automaticamente" intercettando il meccanismo di routing dell'API Web con il proprio, come con un codice come questo, nel globale. asax.cs:

GlobalConfiguration.Configuration.Services.Replace(
                typeof(IHttpControllerActivator), new WindsorCompositionRoot(_container));

Point: global.asax.cs 'Application_Start () lancia la palla e chiama il programma di installazione / registrazione con codice come:

protected void Application_Start()
{            
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);

    ConfigureWindsor(GlobalConfiguration.Configuration);
}

public static void ConfigureWindsor(HttpConfiguration configuration)
{
    _container = new WindsorContainer();
    _container.Install(FromAssembly.This());
    _container.Kernel.Resolver.AddSubResolver(new CollectionResolver(_container.Kernel, true));
    var dependencyResolver = new WindsorDependencyResolver(_container);
    configuration.DependencyResolver = dependencyResolver;
}   

Punto: Molte altre impalcature di Castle Windsor devono essere installate per questo. Nello specifico, le classi devono essere "installate" (registrato - credo che sarebbe stato registrato come termine migliore / più facile da usare).

Questa è una delle mie principali aree "say what?" / headscratching. Non so se il codice di registrazione dovrebbe apparire così:

public object GetService(Type serviceType)
{
    return _container.Kernel.HasComponent(serviceType) ? _container.Resolve(serviceType) : null;
}

public IEnumerable<object> GetServices(Type serviceType)
{
    if (!_container.Kernel.HasComponent(serviceType))
    {
        return new object[0];
    }

    return _container.ResolveAll(serviceType).Cast<object>();
}

... o in questo modo:

public class ApiControllersInstaller : IWindsorInstaller
{
    public void Install(Castle.Windsor.IWindsorContainer container, Castle.MicroKernel.SubSystems.Configuration.IConfigurationStore store)
    {
        container.Register(Classes.FromThisAssembly() // should it be Types instead of Classes?
         .BasedOn<ApiController>()
         .LifestylePerWebRequest());
    }
}

... o in questo modo:

public class ServiceInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(
            Component.For<IDeliveryItemRepository>().ImplementedBy<DeliveryItemRepository>().LifestylePerWebRequest(),
            Component.For<IDeliveryRepository>().ImplementedBy<DeliveryRepository>().LifestylePerWebRequest(),
            Component.For<IDepartmentRepository>().ImplementedBy<DepartmentRepository>().LifestylePerWebRequest(),
            Component.For<IExpenseRepository>().ImplementedBy<ExpenseRepository>().LifestylePerWebRequest(),
            Component.For<IInventoryItemRepository>().ImplementedBy<InventoryItemRepository>().LifestylePerWebRequest(),
            Component.For<IInventoryRepository>().ImplementedBy<InventoryRepository>().LifestylePerWebRequest(),
            Component.For<IItemGroupRepository>().ImplementedBy<ItemGroupRepository>().LifestylePerWebRequest());
    }
}

... o altrimenti ...

Point: una volta che tutto questo è stato impostato, e i Controller (e i Repository? C'è più di quello che ho su di loro, per renderli sicuri per CW?) sono stati registrati dal motore di routing Castle Windsor, una chiamata alla mia app Web API come:

http://platypus:28642/api/Departments

... verrà risolto in un "componente" (classe concreta) che implementa IDepartmentRepository. CW sarà in grado di capirlo, finché avrò una classe che implementa IDepartmentRepository, che è quella che passa silenziosamente al costruttore del Controller che accetta un argomento per una classe che implementa IDepartmentRepository.

Ma ... cosa succede se ci sono classi N che implementano IDepartmentRepository? In che modo CW sa quale passerà al costruttore del Controller? Come posso, l'umile umano in questa riunione di silicio e materiale rosa squishy, indicare quale dei N voglio che CW passi al costruttore?

    
posta B. Clay Shannon 09.01.2014 - 01:43
fonte

2 risposte

7

Ho esperienza con Ninject, non con Castle Windsor, ma i principi dovrebbero essere gli stessi.

Per comprendere l'iniezione di dipendenza, aiuta a ricordare come hai scritto il codice senza di esso. In genere, il costruttore di DepartmentsController avrà un aspetto simile al seguente:

public DepartmentsController()
{
    _repository = new DepartmentRepository();
}

Il controllore dipende dal repository e posizionandolo nel costruttore ci assicuriamo che ogni DepartmentsController sia istanziato con un repository e inizi la sua vita in uno stato valido. In questo modo, tuttavia, è più difficile scambiare in seguito un'implementazione di DepartmentRepository con un'altra. In particolare, se vogliamo isolare DepartmentsController e testarlo in modo indipendente, siamo nei guai. Come testiamo questa classe senza invocare chiamate effettive al database attraverso DepartmentRepository ?

Il tuo primo esempio migliora questo scenario creando una "cucitura" tra gli oggetti: un punto in cui i due sono incollati o cuciti insieme che possono essere separati facilmente, specialmente per i test. Invece di creare DepartmentRepository , il controller può richiederlo:

public object GetService(Type serviceType)
{
    return _container.Kernel.HasComponent(serviceType) ? _container.Resolve(serviceType) : null;
}

Questo è chiamato il modello di localizzazione del servizio, e alcuni lo considerano una cattiva pratica. Esponendo Castle Windsor in questo modo, hai effettivamente perso alcuni dei suoi benefici. In particolare, il tuo DepartmentsController dipende ora dal Service Locator e dal suo metodo GetService . Non coprirò tutti i presunti svantaggi dei Service Locator, ma consideri cosa succede quando la tua applicazione cresce e ora hai numerose classi (potenzialmente tutte) dipendenti da questa singola classe.

Il tuo terzo esempio migliora il modello di localizzazione del servizio rendendo il contenitore invisibile alla tua classe:

public DepartmentsController(IDepartmentRepository repository)
{
    if (repository == null)
        throw new ArgumentNullException("repository");

    _repository = repository;
}

Si noti come questa versione non abbia conoscenza del kernel / container o di un localizzatore di servizi. Invece, abbiamo reso le sue vere dipendenze esplicite nel costruttore piuttosto che i mezzi per quelle dipendenze. Infatti, scrivendo le tue classi in questo modo, puoi costruire intere librerie di classi che sono ignoranti di Castle Windsor e funzioneranno perfettamente con Ninject, un altro framework DI o nessun contenitore, senza modificare il codice.

Come si registrano effettivamente i componenti (la differenza tra gli esempi due e tre) è parzialmente una questione di scala.

container.Register(
        Component.For<IDeliveryItemRepository>().ImplementedBy<DeliveryItemRepository>().LifestylePerWebRequest(),
        Component.For<IDeliveryRepository>().ImplementedBy<DeliveryRepository>().LifestylePerWebRequest(),
        Component.For<IDepartmentRepository>().ImplementedBy<DepartmentRepository>().LifestylePerWebRequest(),
...

va bene per un'applicazione più piccola. Se il numero di classi cresce, devi affrontare una lista infinita di associazioni o trovare un metodo migliore. Un miglioramento consiste nel favorire la convenzione rispetto alla configurazione: se Castle Windor assume che ogni IDeliverItemRepository abbia un'implementazione DeliveryItemRepository , è possibile saltare le associazioni precedenti. Tuttavia, non hai risolto nulla se la maggior parte delle tue classi richiede una configurazione eccezionale; hai ancora un metodo di registrazione del kernel / contenitore lungo e onnisciente.

Invece, dovresti probabilmente esplorare qualcosa come il tuo secondo esempio. In questo modo, il tuo metodo ConfigureWindsor può utilizzare la reflection per trovare qualsiasi IWindsorInstaller s presente nell'applicazione senza conoscere interfacce o implementazioni specifiche. Puoi rompere tutti i tuoi binding in moduli separati (il nome di Ninject per questo concetto) che vengono scoperti in fase di runtime.

Per rispondere alla tua ultima domanda: Castle Windsor non saprà quali delle tue implementazioni N scegliere senza che tu lo dica. Puoi farlo attraverso una combinazione dei concetti discussi sopra: principalmente per convenzione, ma con IWindsorInstaller s quando necessario per specificare dettagli importanti come il ciclo di vita e binding condizionali .

    
risposta data 09.01.2014 - 03:02
fonte
4

CW sa quali classi N implementano IDepartmentRepository tramite reflection. Conosce quale vuoi perché lo hai registrato - questo è il punto di registrazione. È abbastanza intelligente, guarda i costruttori di ciò che hai registrato e cerca di dedurre i tipi concreti basati su ciò che hai registrato. Quindi a volte registrare la parte superiore di un albero delle dipendenze sarà sufficiente per far capire il resto.

Ci sono diversi modi per registrarti, per tua comodità (e confusione). Utilizzare un'API fluente per comodità (e confusione). Puoi anche controllare le cose sul ciclo di vita delle dipendenze (immagina che una dipendenza abbia bisogno di chiamare dispose (), ma la classe che riceve la dipendenza non la crea e non può sapere del suo ciclo di vita - comunque un argomento più avanzato) .

Dovresti registrare le classi concrete che vuoi, quindi risolverle nel tuo metodo Main (nel tuo caso Application_Start serve a questo scopo), questo creerà immediatamente tutti gli oggetti nel grafico delle dipendenze. Chiama il rilascio sul contenitore quando hai finito. Non chiamare il contenitore da nessun'altra parte nell'app.

Spero che questo aiuti: c'erano molte domande.

Personalmente mi è piaciuto Iniezione delle dipendenze in .NET come riferimento (nessuna affiliazione con me), ma non è 't gratuito.

    
risposta data 09.01.2014 - 02:14
fonte