2010-04-04 25 views
8

Io e altri due colleghi stiamo cercando di capire come progettare al meglio un programma. Ad esempio, ho un'interfaccia ISoda e classi multiple che implementano tale interfaccia come Coke, Pepsi, DrPepper, ecc ....Utilizzare correttamente dipendenza dipendenza

mio collega sta dicendo che è meglio mettere questi elementi in un database come una chiave/valore paio. Per esempio:

Key  | Name 
-------------------------------------- 
Coke  | my.namespace.Coke, MyAssembly 
Pepsi  | my.namespace.Pepsi, MyAssembly 
DrPepper | my.namespace.DrPepper, MyAssembly 

... quindi avere i file di configurazione XML che mappano l'input per la chiave corretta, interrogare il database per la chiave, quindi creare l'oggetto.

Non ho motivi specifici, ma mi sembra che questo sia un cattivo progetto, ma non so cosa dire o come argomentare correttamente contro di esso.

Il mio secondo collega sta suggerendo di gestire micro ognuna di queste classi. Quindi, in pratica l'ingresso sarebbe passare attraverso uno switch, qualcosa di simile a questa:

ISoda soda; 

switch (input) 
{ 
    case "Coke": 
     soda = new Coke(); 
     break;  
    case "Pepsi": 
     soda = new Pepsi(); 
     break; 
    case "DrPepper": 
     soda = new DrPepper(); 
     break; 
} 

questo sembra un po 'meglio per me, ma io continuo a pensare che ci sia un modo migliore per farlo. Ho letto i contenitori IoC negli ultimi giorni e sembra una buona soluzione. Tuttavia, sono ancora molto nuovo all'input di dipendenza e ai contenitori IoC, quindi non so come discuterlo correttamente. O forse sono io quello sbagliato e c'è un modo migliore per farlo? Se è così, qualcuno può suggerire un metodo migliore?

Che tipo di argomenti posso portare sul tavolo per convincere i miei colleghi a provare un altro metodo? Quali sono i pro/contro? Perché dovremmo farlo in un modo?

Sfortunatamente, i miei colleghi sono molto resistenti al cambiamento, quindi sto cercando di capire come posso convincerli.

Edit:

Sembra che la mia domanda è un po 'difficile da capire. Quello che stiamo cercando di capire è come progettare al meglio un'applicazione che utilizza più interfacce e contiene molti tipi concreti che implementano tali interfacce.

Sì, so che archiviare questa roba nel database è una cattiva idea, ma semplicemente non conosciamo un modo migliore. Questo è il motivo per cui sto chiedendo a come facciamo.

public void DrinkSoda(string input) 
{ 
    ISoda soda = GetSoda(input); 
    soda.Drink(); 
} 

Come implementare correttamente GetSoda? Dovremmo ripensare l'intero design? Se è una cattiva idea passare le stringhe magiche come questa, allora come dovremmo farlo?

Gli input utente come "Diet Coke", "Coca Cola", "Coca Cola" o qualsiasi altro istanzializza un tipo di Coke, quindi più parole chiave vengono mappate su un singolo tipo.

Un sacco di persone hanno detto che il codice pubblicato sopra è cattivo. So che è cattivo. Sto cercando dei motivi per presentare ai miei colleghi e discutere perché è male e perché dobbiamo cambiare.

Mi scuso se questa spiegazione è ancora difficile da capire. È difficile per me descrivere la situazione perché non capisco davvero come metterla in parole.

+3

È davvero difficile capire quale sia il vero problema che stai cercando di risolvere. – mquander

+1

Il secondo blocco di codice sembra qualcosa che si vedrebbe quando si utilizza il modello di fabbrica. Stai chiedendo come determinare il tipo di ISode passato a te? – NitroxDM

+2

Wow - memorizzando i nomi dei tipi nel database, utilizzando un file XML per mappare le parole ai record del database e quindi usando il reflection per istanziarli - ora ** that ** is ** enterprisey! ** – Aaronaught

risposta

4

La migliore introduzione all'iniezione di dipendenza che ho riscontrato è in realtà lo quick series of articles that form a walkthrough of the concepts nel sito wiki Ninject.

Non hai dato molto sfondo a quello che stai cercando di realizzare, ma mi sembra che tu stia cercando di progettare un'architettura "plug-in". Se questo è il caso, la tua sensazione è corretta - entrambi questi disegni sono in una parola, orribili. Il primo sarà relativamente lento (database e reflection?), Ha enormi dipendenze non necessarie su un livello di database e non si ridimensionerà con il programma, poiché ogni nuova classe dovrà essere inserita nel database. Cosa c'è che non va usando solo la riflessione?

Il secondo approccio riguarda l'invalicabile. Ogni volta che introduci una nuova classe, il vecchio codice deve essere aggiornato e qualcosa deve "sapere" su ogni oggetto che verrà collegato, rendendo impossibile a terzi lo sviluppo di plug-in. L'accoppiamento che aggiungi, anche con qualcosa come un pattern Locator, significa che il tuo codice non è flessibile come potrebbe essere.

Questa potrebbe essere una buona opportunità per utilizzare l'iniezione di dipendenza. Leggi gli articoli che ho linkato. L'idea chiave è che il framework di iniezione delle dipendenze utilizzerà un file di configurazione per caricare dinamicamente le classi specificate. Ciò rende lo scambio di componenti del tuo programma molto semplice e diretto.

3

Sarò onesto, e so che gli altri non sono d'accordo, ma trovo che astrarre l'istanza di classe in XML o in un database sia un'idea orribile. Sono un fan del codice cristallino anche se sacrifica una certa flessibilità. Gli aspetti negativi che vedo sono:

  • E 'difficile tracciare attraverso il codice e trovare dove si creano oggetti.
  • Altre persone che lavorano al progetto sono tenuti a comprendere il formato proprietario per la creazione di istanze di oggetti.
  • Su piccoli progetti si impiegherà più tempo per implementare l'integrazione delle dipendenze in modo da passare effettivamente alle classi.
  • Non sarà possibile stabilire cosa viene creato fino al runtime. Questo è un incubo di debug, e neanche il tuo debugger ti piacerà.

Sono davvero un po 'stanco di sentire parlare di questa particolare moda di programmazione. Sembra e sembra brutto, e risolve un problema che alcuni progetti devono aver risolto. Seriamente, quanto spesso aggiungerai e rimuoverai le bibite da quel codice. A meno che non sia praticamente ogni volta che si compila, c'è così poco vantaggio con così tanto extra complxexity.

Giusto per chiarire, penso che l'inversione di controllo possa essere una buona idea di design. Semplicemente non mi piace avere il codice esternamente incorporato solo per l'istanziazione, in XML o in altro modo.

La soluzione switch-case va bene, in quanto è un modo di implementare il modello di fabbrica. La maggior parte dei programmatori non sarà difficile comprenderla. È anche più chiaramente organizzato di istanziare sottoclassi in tutto il codice.

+0

Questo è uno dei motivi per cui mi piace Ninject come contenitore DI. La registrazione viene eseguita in un modulo di codice utilizzando un'interfaccia fluente, non un file di configurazione XML o un database. Lo stesso con Autofac. – Aaronaught

+3

@aaronaught, penso che ogni contenitore eccetto Spring.NET, incl. Unity può fare la registrazione in codice, la maggior parte di essi (tranne Unity e Spring) può fare la registrazione basata su convenzione, quindi non penso che sia un grosso problema - è di fatto uno standard. –

3

Ogni libreria DI ha una propria sintassi per questo.Ninject è simile al seguente:

public class DrinkModule : NinjectModule 
{ 
    public override void Load() 
    { 
     Bind<IDrinkable>() 
      .To<Coke>() 
      .Only(When.ContextVariable("Name").EqualTo("Coke")); 
     Bind<IDrinkable>() 
      .To<Pepsi>() 
      .Only(When.ContextVariable("Name").EqualTo("Pepsi")); 
     // And so on 
    } 
} 

Naturalmente, diventa piuttosto noioso per mantenere le mappature, quindi se ho questo tipo di sistema, allora io di solito finiscono per mettere insieme una registrazione riflessione basata simile a this one:

public class DrinkReflectionModule : NinjectModule 
{ 
    private IEnumerable<Assembly> assemblies; 

    public DrinkReflectionModule() : this(null) { } 

    public DrinkReflectionModule(IEnumerable<Assembly> assemblies) 
    { 
     this.assemblies = assemblies ?? 
      AppDomain.CurrentDomain.GetAssemblies(); 
    } 

    public override void Load() 
    { 
     foreach (Assembly assembly in assemblies) 
      RegisterDrinkables(assembly); 
    } 

    private void RegisterDrinkables(Assembly assembly) 
    { 
     foreach (Type t in assembly.GetExportedTypes()) 
     { 
      if (!t.IsPublic || t.IsAbstract || t.IsInterface || t.IsValueType) 
       continue; 
      if (typeof(IDrinkable).IsAssignableFrom(t)) 
       RegisterDrinkable(t); 
     } 
    } 

    private void RegisterDrinkable(Type t) 
    { 
     Bind<IDrinkable>().To(t) 
      .Only(When.ContextVariable("Name").EqualTo(t.Name)); 
    } 
} 

Leggermente più codice ma non è necessario aggiornarlo, diventa auto-registrazione.

Oh, e si utilizza in questo modo:

var pop = kernel.Get<IDrinkable>(With.Parameters.ContextVariable("Name", "Coke")); 

Ovviamente questo è specifico per una libreria singola DI (Ninject), ma tutti li sostengono il concetto di variabili o parametri, e ci sono modelli equivalenti per tutti loro (Autofac, Windsor, Unità, ecc)

Quindi la risposta breve alla tua domanda è, in fondo, sì, dI possono fare questo, ed è un buon posto per avere questo tipo di logica , sebbene nessuno degli esempi di codice nella tua domanda originale abbia effettivamente qualcosa a che fare con DI (il secondo è solo un metodo di fabbrica, e il primo è ... beh, impresa).

+0

Sì, mi rendo conto che il codice postato non ha nulla a che fare con DI. Sto chiedendo dei buoni motivi per cui dovremmo o non dovremmo passare a DI per gestire scenari come questo. Sono assolutamente d'accordo sul fatto che il codice postato sia sbagliato e vorrei presentare buoni argomenti da presentare ai miei colleghi. – Rune

+0

@Rune: ho letto la tua modifica e penso che la mia risposta sia essenzialmente la stessa; se stai cercando un modo migliore per farlo, la mia risposta sarebbe * usare una libreria DI *. L'argomento principale a favore è (a) la manutenzione e (b) è * facile *. E possibilmente (c) è molto meno probabile essere buggato di qualcosa che scrivi tu stesso. Se nessuna di queste cose è importante per te, utilizzerei solo il metodo factory. – Aaronaught

7

Come regola generale, è più orientato agli oggetti passare intorno agli oggetti che racchiudono concetti che passare attorno alle stringhe. Detto questo, ci sono molti casi (in particolare quando si tratta di interfaccia utente) quando è necessario tradurre dati sparsi (come le stringhe) in oggetti dominio appropriati.

Ad esempio, è necessario convertire l'input dell'utente in una stringa in un'istanza ISoda.

Il modo standard e liberamente accoppiato per eseguire questa operazione è l'utilizzo di Abstract Factory. Definire in questo modo:

public interface ISodaFactory 
{ 
    ISoda Create(string input); 
} 

vostro consumatore può ora prendere una dipendenza da ISodaFactory e utilizzarlo, in questo modo:

public class SodaConsumer 
{ 
    private readonly ISodaFactory sodaFactory; 

    public SodaConsumer(ISodaFactory sodaFactory) 
    { 
     if (sodaFactory == null) 
     { 
      throw new ArgumentNullException(sodaFactory); 
     } 

     this.sodaFactory = sodaFactory; 
    } 

    public void DrinkSoda(string input) 
    { 
     ISoda soda = GetSoda(input); 
     soda.Drink(); 
    } 

    private ISoda GetSoda(input) 
    { 
     return this.sodaFactory.Create(input); 
    } 
} 

Notate come la combinazione della clausola di guardia e la parola chiave readonly garantisce la SodaConsumer invarianti di classe, che ti permettono di usare la dipendenza senza preoccuparti che possa essere nullo.

+0

Forse ho appena frainteso la domanda, ma ho pensato che stesse chiedendo come implementare il metodo 'ISodaFactory.Create' - che è dove sono finite tutte le istruzioni' switch' o il codice di riflessione. – Aaronaught

4

Questo è il modello che di solito uso se ho bisogno di creare un'istanza della classe corretta sulla base di un nome o altri dati serializzati:

public interface ISodaCreator 
{ 
    string Name { get; } 
    ISoda CreateSoda(); 
} 

public class SodaFactory 
{ 
    private readonly IEnumerable<ISodaCreator> sodaCreators; 

    public SodaFactory(IEnumerable<ISodaCreator> sodaCreators) 
    { 
     this.sodaCreators = sodaCreators; 
    } 

    public ISoda CreateSodaFromName(string name) 
    { 
     var creator = this.sodaCreators.FirstOrDefault(x => x.Name == name); 
     // TODO: throw "unknown soda" exception if creator null here 

     return creator.CreateSoda(); 
    } 
} 

Nota che sarà ancora bisogno di fare un sacco di ISodaCreator implementazioni, e in qualche modo raccogliere tutte le implementazioni da passare al costruttore SodaFactory. Per ridurre il lavoro necessario in quanto, è possibile creare un unico creatore soda generico sulla base di riflessione:

public ReflectionSodaCreator : ISodaCreator 
{ 
    private readonly ConstructorInfo constructor; 

    public string Name { get; private set; } 

    public ReflectionSodaCreator(string name, ConstructorInfo constructor) 
    { 
     this.Name = name; 
     this.constructor = constructor; 
    } 

    public ISoda CreateSoda() 
    { 
     return (ISoda)this.constructor.Invoke(new object[0]) 
    } 
} 

e poi basta eseguire la scansione del gruppo di esecuzione (o qualche altro insieme di assemblee) per trovare tutti i tipi di soda e costruire un SodaFactory come questo:

IEnumerable<ISodaCreator> sodaCreators = 
    from type in Assembly.GetExecutingAssembly().GetTypes() 
    where type.GetInterface(typeof(ISoda).FullName) != null 
    from constructor in type.GetConstructors() 
    where constructor.GetParameters().Length == 0 
    select new ReflectionSodaCreator(type.Name, constructor); 
sodaCreators = new List<ISodaCreator>(sodaCreators); // evaluate only once 
var sodaFactory = new SodaFactory(sodaCreators); 

Nota che la mia risposta complementare a quello di Marco Seemann: egli ti dice di lasciare che i clienti utilizzare un abstract ISodaFactory, in modo che essi non devono preoccuparsi circa how opere soda fabbrica.La mia risposta è un esempio di implementazione di una simile fabbrica di soda.

3

Penso che il problema non sia nemmeno una fabbrica o altro; è la tua struttura di classe.

DrPepper si comporta diversamente da Pepsi? E Coke? Per me, sembra che tu stia usando le interfacce in modo improprio.

A seconda della situazione, farei una classe Soda con una proprietà BrandName o simile. Lo BrandName sarà impostato su DrPepper o Coke, tramite uno enum o uno string (il primo sembra una soluzione migliore per il tuo caso).

Risolvere il problema di fabbrica è facile. In effetti, non hai affatto bisogno di una fabbrica. Basta usare il costruttore della classe Soda.

+0

Sì, si comporterebbero in modo diverso. Ho solo cercato di rendere il mio esempio il più generico possibile. Ma nel mondo reale, il metodo di interfaccia "Drink" sarebbe implementato in modo diverso per ogni soda. – Rune

Problemi correlati