7

E 'possibile e/o una buona idea di utilizzare Ninject (o qualsiasi altro contenitore CIO, è per questo) per creare un legamedi default per situazioni in cui non esiste un'implementazione appropriata e utilizzare questo bind predefinito piuttosto che gestire uno ActivationException quando esistono più associazioni o non esistono associazioni per una particolare richiesta?Utilizzando Ninject e vincolante di un'implementazione di default, evitando la temuta Service Locator anti-modello

Sono stato utilizzando Factory e Conventions progetti di ampliamento di Ninject, ma mi chiedo se stanno mascherando un errore che sto facendo a un livello più fondamentale, così ho creato un test per illustrare quello che voglio di fare, nel modo più semplice che posso:

Dato il seguente:

public interface IWidget { } 
public class DefaultWidget : IWidget { } 
public class BlueWidget : IWidget { } 

E il xUnit prova seguente utilizzando FluentAssertions:

[Fact] 
public void Unknown_Type_Names_Resolve_To_A_Default_Type() 
{ 
    StandardKernel kernel = new StandardKernel(); 

    // intention: resolve a `DefaultWidget` implementation whenever the 
    // 'name' parameter does not match the name of any other bound implementation 
    kernel.Bind<IWidget>().To<DefaultWidget>(); 
    kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name); 
    kernel.Get<IWidget>("RedWidget").Should().BeOfType<DefaultWidget>(); 

    //  ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`) 
} 

Non so esattamente come percorrere questa strada, se è possibile, mi porterebbe a una sgradevole implementazione del modello di Localizzatore di servizi, a parte il fatto che sembra essere un avvertimento dato da coloro che hanno risposto a domande simili.

Quindi, è un abuso/uso improprio di contenitori IoC per fare ciò che sto chiedendo di fare?

Sembra che dovrei voler associare/risolvere tipi con un IoC per tutti i tipi espliciti che ho, quindi quella parte non mi sembra sbagliata. Nella vita reale, ci sarebbero attacchi molti di più espliciti come BlueWidget, così come un numero imprecisato di variazioni sul valore stringa "RedWidget" in arrivo.

In generale, sembra che l'idea di un'implementazione predefinita di alcune interfacce è una situazione non rara, quindi dove sarebbe questo meccanismo per la risoluzione delle richieste, se non all'interno del regno del contenitore IoC?

Ho anche intenzione di utilizzare il modello di fabbrica per creare le implementazioni IWidget. Finora, ho utilizzato le fabbriche create automaticamente da Ninject.Extensions.Factory con provider di istanze personalizzati e generatori di binding personalizzati, ma non posso superare questo problema.

Avrebbe maggiore controllo sull'implementazione di fabbrica (in altre parole, utilizzare la mia fabbrica, non la fabbrica automatica da Ninject.Extensions.Factory)? In passato, ho utilizzato Assembly reflection per trovare i tipi di candidati e utilizzare Activation.CreateInstance() per creare l'implementazione specifica di cui ho bisogno o un'implementazione predefinita, ma questo diventa davvero ingombrante una volta che queste implementazioni hanno le proprie dipendenze con l'aggiunta del costruttore come principi di iniezione di dipendenza vengono applicati a tali implementazioni. Quindi, il mio passaggio ai contenitori IoC per una soluzione - ma questo non sta funzionando come avrei sperato.

UPDATE 1 - successo con mia propria fabbrica ATTUAZIONE

io non sono davvero contento di questo, perché ogni volta che una nuova implementazione di IWidget deve essere scritto, dovrò crack aprire questa fabbrica e aggiornalo anche tu. Nel mio esempio qui, dovrei aggiungere anche un'altra riga nei binding, ma è qui che entra in gioco l'associazione basata sulla convenzione, che avrei pianificato di utilizzare per evitare di dover aggiornare costantemente le definizioni di binding.

Utilizzando questa implementazione fabbrica,

public interface IWidgetFactory { IWidget Create(string name); } 
public class WidgetFactory : IWidgetFactory 
{ 
    private readonly IKernel kernel; 
    public WidgetFactory(IKernel kernel) { this.kernel = kernel; } 

    public IWidget Create(string name) 
    { 
     switch (name) 
     { 
      case "Blue": 
       return this.kernel.Get<IWidget>(typeof (BlueWidget).Name); 
      default: 
       return this.kernel.Get<IWidget>(typeof (DefaultWidget).Name); 
     } 
    } 
} 

posso ottenere questa prova da superare:

[Fact] 
public void WidgetBuilders_And_Customizers_And_Bindings_Oh_My() 
{ 
    StandardKernel kernel = new StandardKernel(); 
    kernel.Bind<IWidget>().To<DefaultWidget>().Named(typeof(DefaultWidget).Name); 
    kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof (BlueWidget).Name); 
    kernel.Bind<IWidgetFactory>().To<WidgetFactory>().InSingletonScope(); 

    kernel.Get<IWidgetFactory>().Create("Blue") 
     .Should().BeOfType<BlueWidget>(); 
    kernel.Get<IWidgetFactory>().Create("Red") 
     .Should().BeOfType<DefaultWidget>(); 
} 

Funziona, ma non si sente bene, per i seguenti motivi:

  1. Devo iniettare lo IKernel nello IWidgetFactory
  2. Per ogni nuova implementazione del IWidget, il IWidgetFactory deve essere aggiornato
  3. Sembra un'estensione Ninject dovrebbe essere stato creato per questo scenario già

FINE UPDATE 1

Cosa sarebbe si fa in questa situazione, considerando che il conteggio delle implementazioni IWidget è alto, l'intervallo previsto di argomenti "nome widget" è essenzialmente infinito e tutti i nomi di widget non risolvibili devono essere gestiti w con il DefaultWidget?

Non c'è bisogno di leggere oltre, ma se siete interessati, ecco le varie prove ho provato come ho lavorato su questo problema:

Ecco l'evoluzione completa di test che ho passato:

[Fact] 
public void Unknown_Type_Names_Resolve_To_A_Default_Type() 
{ 
    StandardKernel kernel = new StandardKernel(); 

    // evolution #1: simple as possible 
    //  PASSES (as you would expect) 
    //kernel.Bind<IWidget>().To<BlueWidget>(); 
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>(); 

    // evolution #2: make the only binding to a different IWidget 
    //  FAILS (as you would expect) 
    //kernel.Bind<IWidget>().To<DefaultWidget>(); 
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>(); 

    // evolution #3: add the binding to `BlueWidget` back 
    //  ACTIVATION EXCEPTION (more than one binding for `IWidget`) 
    //kernel.Bind<IWidget>().To<DefaultWidget>(); 
    //kernel.Bind<IWidget>().To<BlueWidget>(); 
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>(); 

    // evolution #4: make `BlueWidget` binding a *named binding* 
    //  ACTIVATION EXCEPTION (more than one binding for `IWidget`) 
    //kernel.Bind<IWidget>().To<DefaultWidget>(); 
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof (BlueWidget).Name); 
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>(); 

    // evolution #5: change `Get<>` request to specifiy widget name 
    //  PASSES (yee-haw!) 
    //kernel.Bind<IWidget>().To<DefaultWidget>(); 
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name); 
    //kernel.Get<IWidget>("BlueWidget").Should().BeOfType<BlueWidget>(); 

    // evolution #6: make `BlueWidget` binding *non-named* 
    //  ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`) 
    //kernel.Bind<IWidget>().To<DefaultWidget>(); 
    //kernel.Bind<IWidget>().To<BlueWidget>(); 
    //kernel.Get<IWidget>("BlueWidget").Should().BeOfType<BlueWidget>(); 

    // evolution #7: ask for non-existance `RedWidget`, hope for `DefaultWidget` 
    //  ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`) 
    //kernel.Bind<IWidget>().To<DefaultWidget>(); 
    //kernel.Bind<IWidget>().To<BlueWidget>(); 
    //kernel.Get<IWidget>("RedWidget").Should().BeOfType<DefaultWidget>(); 

    // evolution #8: make `BlueWidget` binding a *named binding* again 
    //  ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`) 
    //kernel.Bind<IWidget>().To<DefaultWidget>(); 
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name); 
    //kernel.Get<IWidget>("RedWidget").Should().BeOfType<DefaultWidget>(); 

    // evolution #9: remove `RedWidget` specification in Get<> request 
    //  ACTIVATION EXCEPTION (back to **more than one** binding for `IWidget`) 
    //kernel.Bind<IWidget>().To<DefaultWidget>(); 
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name); 
    //kernel.Get<IWidget>().Should().BeOfType<DefaultWidget>(); 
} 
+0

Hai chiesto una grande domanda e hanno chiaramente messo in un sacco di lavoro e io Sono molto felice di +1. Personalmente sono confuso dal risultato di # 8 e verificato manualmente! Il prossimo passo è andare a dare un'occhiata ai test per Named Bindings e a) cercare di capirli per migliorare la tua domanda b) provare a capirlo in modo da poter implementare la tua soluzione usando i tuoi metadati di binding (credimi sarà facile) ma idealmente scoprirai dai test se tutto ciò che stai osservando ha senso, quindi invia un bug report o una Pull Request o apporta una modifica alla tua domanda –

+0

@Ruben, grazie per il commento - questi test sono repository principale di GitHub di Ninject? Non riesco a trovare alcun file di test come "NamedBindingsTests", sebbene abbia localizzato BindingMetadata nella cartella "Planning". – Jeff

+0

Dunno - stavo solo ipotizzando in base alle mie precedenti esplorazioni, 2s ... sembra che https://github.com/ninject/ninject/blob/master/src/Ninject.Test/Integration/StandardKernelTests.cs sia il più vicino. Non riesco a ricordare se c'è una wiki, un post sul blog o una risposta SO ben trafficata sul comportamento di denominazione predefinito come lo stai chiedendo, ma sono abbastanza sicuro che sia da qualche parte (personalmente personalmente ne faccio il nome o nessuno, quindi non è mai sorto come preoccupazione) –

risposta

2

Non so dove questo si accumula, ma funziona. Non puoi passare un parametro con nome, però funziona solo con un parametro vuoto. In altre parole, non è possibile eseguire questa operazione:

var stuff1 = kernel.Get<IWidget>("OrangeWidget"); 

A meno che OrangeWidget esista. Piuttosto per ottenere il valore di default è necessario fare questo:

var stuff2 = kernel.Get<IWidget>(); 

Ecco un esempio:

IDictionary dic = new Dictionary<string, string>(); 

dic.Add("BlueWidget", "BlueWidget"); 
dic.Add("RedWidget", "RedWidget"); 

kernel.Bind<IWidget>().To<DefaultWidget>() 
    .When(x => x.Service.Name != (string)dic[x.Service.Name]); 
kernel.Bind<IWidget>().To<BlueWidget>().Named("BlueWidget"); 

var stuff1 = kernel.Get<IWidget>("BlueWidget"); 
var stuff2 = kernel.Get<IWidget>(); 

This è un Ninject fresco inviare potreste essere interessati a ...

I' Mi piacerebbe aggiungere qualcosa sui parametri nominati. Questo è basato sulla documentazione this. Il parametro Named ti permette di chiamare i tuoi binding per nome quando ottieni <> loro, ma non "default" su nulla. Quindi dovresti effettivamente passare il nome DefaultWidget per ottenere quell'associazione. Questo funziona:

kernel.Bind<IWidget>().To<BlueWidget>().Named("BlueWidget"); 
kernel.Bind<IWidget>().To<DefaultWidget>().Named("DefaultWidget"); 

var blueOne = kernel.Get<IWidget>("BlueWidget"); 
var defaultOne = kernel.Get<IWidget>("DefaultWidget"); 

Se qualcuno riesce a capire come implementare un default, sarei curioso di sapere come, se non ho mai avuto bisogno. Sono uno studente di Ninject e mi piace molto.

UPDATE:

ho capito. Trovato una soluzione interessante here.

ho creato una classe di estendere Ninject:

public static class NinjectExtensions 
{ 
    public static T GetDefault<T>(this IKernel kernel) 
    { 
     return kernel.Get<T>(m => m.Name == null); 
    } 

    public static T GetNamedOrDefault<T>(this IKernel kernel, string name) 
    { 
     T result = kernel.TryGet<T>(name); 

     if (result != null) 
      return result; 

     return kernel.GetDefault<T>(); 
    } 
} 

Qui è il vostro metodo Unknown_Type ...

public static void Unknown_Type_Names_Resolve_To_A_Default_Type() 
{ 
    StandardKernel kernel = new StandardKernel(); 

    IDictionary dic = new Dictionary<string, string>(); 

    dic.Add("BlueWidget", "BlueWidget"); 
    dic.Add("RedWidget", "RedWidget"); 

    kernel.Bind<IWidget>().To<DefaultWidget>().When(x => x.Service.Name != (string)dic[x.Service.Name]); 
    kernel.Bind<IWidget>().To<BlueWidget>().Named("BlueWidget"); 

    // this works! 
    var sup = kernel.GetNamedOrDefault<IWidget>("Not here"); 

    var stuff1 = kernel.Get<IWidget>("BlueWidget"); 
    var stuff2 = kernel.Get<IWidget>(); 
} 
+0

Interessante - il tuo primo esempio sembra simile alla mia "evoluzione # 5" (vedi la mia lista di evoluzioni di test in fondo alla mia domanda), e sono riuscito a farlo passare senza condizioni di binding aggiuntive usando 'When. (X = > ... 'come hai mostrato. Da quando ho postato la domanda, ho iniziato a credere che stavo abusando delle funzionalità di Ninject tentando di inserire la logica di fallback predefinita specifica del tipo nei binding, e ho fatto ricorso a un semplice istruzione switch nel mio codice per recuperare un 'DefaultWidget' quando non è stato in grado di trovare quello specifico che ho richiesto. – Jeff

+0

Potrebbe essere più di una domanda di progettazione generale, e forse ci sono risposte su [Programmatori] (http: // programmatori .stackexchange.com /) o simili, dal momento che parte di questa domanda riguarda le funzionalità di Ninject e l'altra parte ti chiede se questa logica debba essere accoppiata a qualsiasi contenitore IoC - Ninject o altro. – Jeff

+0

Uso effettivamente un IOC C ontainer e metti tutte le mie "nuove" dichiarazioni nei moduli bind. Quindi chiamo tutto come questo IOCCOntainer.instance.Get ().Trovo che organizzi davvero il mio codice e posso passare alla modalità test puntando a un modulo di bind diverso per esempio i miei mock. Faccio schifo a tutti i miei servizi e così, e alcune persone si sentono i mock di terze parti sono migliori. In questo modo il mio primo test è facile. Ninject lo fa facilmente, ecco perché mi piace e perché mi aggiro nei siti come Stackoverflow per imparare più trucchi. La tua domanda è stata interessante perché non uso mai le impostazioni predefinite. – CodeChops