2010-04-04 12 views
8

Yo y otros dos colegas estamos tratando de entender cómo diseñar mejor un programa. Por ejemplo, tengo una interfaz ISoda y múltiples clases que implementan la interfaz como Coke, Pepsi, DrPepper, etc ....Usa correctamente la inyección de dependencia

Mi colega está diciendo que es mejor para poner estos elementos en una base de datos como una llave/valor par. Por ejemplo:

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

... entonces tiene archivos de configuración XML que se asignan a la entrada de la clave correcta, consultar la base de datos de la clave, a continuación, crear el objeto.

No tengo razones específicas, pero siento que este es un mal diseño, pero no sé qué decir ni cómo argumentar correctamente en contra.

Mi segundo colega sugiere que microadministremos cada una de estas clases. Así que, básicamente, la entrada sería ir a través de una sentencia switch, algo similar a esto:

ISoda soda; 

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

Esto parece un poco mejor para mí, pero sigo pensando que hay una mejor manera de hacerlo. He estado leyendo sobre contenedores IoC los últimos días y parece una buena solución. Sin embargo, todavía soy muy nuevo en los paquetes de inyección de dependencia y IoC, por lo que no sé cómo defenderlo correctamente. ¿O tal vez soy el equivocado y hay una mejor manera de hacerlo? Si es así, ¿alguien puede sugerir un método mejor?

¿Qué tipo de argumentos puedo presentar para convencer a mis colegas de que prueben otro método? ¿Cuáles son los pros/contras? ¿Por qué deberíamos hacerlo de una manera?

Desafortunadamente, mis colegas son muy resistentes al cambio, así que estoy tratando de descubrir cómo puedo convencerlos.

Editar:

parece que mi pregunta es un poco difícil de entender. Lo que intentamos averiguar es cómo diseñar mejor una aplicación que utiliza múltiples interfaces y contiene muchos tipos concretos que implementan esas interfaces.

Sí, sé que almacenar estas cosas en la base de datos es una mala idea, pero simplemente no sabemos de una mejor manera. Es por eso que estoy preguntando cómo lo hacemos mejor.

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

¿Cómo podemos implementar correctamente GetSoda? ¿Deberíamos reconsiderar todo el diseño? Si es una mala idea pasar cadenas mágicas como esta, ¿cómo deberíamos hacerlo?

Entradas del usuario como "Diet Coke", "Coke", "Coke Lime", o lo que sea que instancia un tipo de Coke, por lo que varias palabras clave se asignan a un solo tipo.

Mucha gente ha dicho que el código anterior es malo. Sé que es malo. Estoy buscando razones para presentarlas a mis colegas y discutir por qué es malo y por qué tenemos que cambiar.

Pido disculpas si esta explicación aún es difícil de entender. Es difícil para mí describir la situación porque realmente no entiendo cómo expresarlo en palabras.

+3

Es realmente difícil entender cuál es el problema real que está tratando de resolver. – mquander

+1

Su segundo bloque de código se ve como algo que vería al usar el patrón de fábrica. ¿Está preguntando cómo determinar el tipo de ISode que se le pasó? – NitroxDM

+2

Guau - almacenar nombres de tipos en la base de datos, usar un archivo XML para asignar palabras a los registros de la base de datos, y luego usar el reflejo para crear instancias de ellos - ahora ** que ** es ** empresarial! ** – Aaronaught

Respuesta

4

La mejor introducción a la inyección de dependencia que me he encontrado es en realidad el quick series of articles that form a walkthrough of the concepts en el sitio wiki Ninject.

Realmente no has dado mucha información sobre lo que estás tratando de lograr, pero me parece que estás tratando de diseñar una arquitectura "plug-in". Si este es el caso, tu sensación es correcta, ambos diseños son en una palabra, horribles. El primero será relativamente lento (base de datos y ¿reflexión?), Tiene dependencias masivas innecesarias en una capa de base de datos, y no se escalará con su programa, ya que cada nueva clase tendrá que ser ingresada en la base de datos. ¿Qué pasa con solo usar el reflejo?

El segundo enfoque se trata de uncalable ya que se trata. Cada vez que introduce una nueva clase, el código antiguo debe actualizarse, y algo tiene que "saber" sobre cada objeto que va a conectar, lo que hace imposible que terceros desarrollen complementos. El acoplamiento que agrega, incluso con algo parecido a un patrón de Localizador, significa que su código no es tan flexible como podría ser.

Esta puede ser una buena oportunidad para usar la inyección de dependencia. Lee los artículos que he vinculado. La idea clave es que el marco de inyección de dependencias utilizará un archivo de configuración para cargar dinámicamente las clases que especifique. Esto hace que el intercambio de componentes de su programa sea muy sencillo y directo.

3

Seré honesto, y sé que otros no están de acuerdo, pero considero que la creación de una instancia de clase de abstracción en XML o una base de datos es una idea horrible. Soy fanático de hacer que el código sea claro incluso si sacrifica cierta flexibilidad. Las desventajas que veo son:

  • Es difícil rastrear el código y encontrar dónde se crean los objetos.
  • Se requiere que otras personas que trabajen en el proyecto entiendan su formato patentado para crear instancias de objetos.
  • En proyectos pequeños, pasará más tiempo implementando la inyección de dependencia que realmente cambiará las clases.
  • No podrá ver lo que se está creando hasta el tiempo de ejecución. Esa es una pesadilla de depuración, y su depurador no le gustará tampoco.

Estoy un poco cansado de escuchar acerca de esta particular moda de programación. Se siente y se ve feo, y resuelve un problema que pocos proyectos necesitan resolver. En serio, con qué frecuencia vas a agregar y eliminar refrescos de ese código. A menos que sea prácticamente cada vez que compilas, hay tan poca ventaja con tanta flexibilidad adicional.

Solo para aclarar, creo que la inversión del control puede ser una buena idea de diseño. Simplemente no me gusta tener un código incrustado externamente solo para creación de instancias, en XML o de otro modo.

La solución del conmutador está bien, ya que es una forma de implementar el patrón de fábrica. La mayoría de los programadores no tendrán dificultades para entenderlo. También está organizado más claramente que la creación de instancias de subclases alrededor de tu código.

+0

Esta es una de las razones por las que me gusta Ninject como contenedor DI. El registro se realiza en un módulo de código utilizando una interfaz fluida, no un archivo de configuración XML o una base de datos. Lo mismo con Autofac. – Aaronaught

+3

@aaronaught, creo que cualquier contenedor excepto Spring.NET, incl. Unity puede hacer el registro en código, la mayoría de ellos (excepto Unity y Spring) pueden hacer un registro basado en convenciones, así que no creo que sea tan importante, es un estándar de facto. –

3

Cada biblioteca DI tiene su propia sintaxis para esto.Ninject tiene el siguiente aspecto:

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 
    } 
} 

Por supuesto, se vuelve bastante tedioso para mantener las asignaciones, por lo que si tengo este tipo de sistema, entonces por lo general terminan la elaboración de un registro basado en la reflexión similar 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)); 
    } 
} 

Un poco más de código pero nunca tiene que actualizarlo, se vuelve a registrar automáticamente.

Ah, y lo utiliza como esto:

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

Obviamente, esto es específico de una biblioteca única DI (Ninject), pero todos ellos apoyan la noción de variables o parámetros, y hay patrones equivalentes para todos ellos (Autofac, Windsor, Unidad, etc.)

Así que la respuesta corta a su pregunta es, básicamente, sí, DI puede hacer esto, y es un muy buen lugar para tener este tipo de lógica , aunque ninguno de los ejemplos de código en su pregunta original realmente tiene algo que ver con DI (el segundo es solo un método de fábrica, y el primero es ... bueno, emprendedor).

+0

Sí, me doy cuenta de que el código publicado no tiene nada que ver con DI. Pregunto por buenas razones por las que deberíamos o no cambiar a DI para manejar escenarios como este. Estoy absolutamente de acuerdo en que el código publicado es malo, y me gustaría presentar buenos argumentos a mis colegas. – Rune

+0

@Rune: He leído su edición y creo que mi respuesta es básicamente la misma; Si está buscando una forma mejor de hacerlo, mi respuesta sería * usar una biblioteca DI *. El argumento principal a favor es (a) mantenimiento y (b) es * fácil *. Y posiblemente (c) es mucho menos probable que tenga errores que algo que usted mismo escriba. Si ninguna de esas cosas le importa, simplemente usaría el método de fábrica. – Aaronaught

7

Como regla general, está más orientado a objetos para pasar objetos que encapsulan conceptos que pasar cadenas. Dicho esto, hay muchos casos (especialmente cuando se trata de la interfaz de usuario) cuando necesita traducir datos dispersos (como cadenas de caracteres) a objetos de dominio adecuados.

Como ejemplo, debe traducir la entrada del usuario en forma de cadena a una instancia de ISoda.

La forma estándar, poco compacta de hacer esto es mediante el empleo de un Abstract Factory. Definirlo así:

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

Su consumo puede ahora tener una dependencia de ISodaFactory y utilizarla, así:

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); 
    } 
} 

Aviso cómo la combinación de la Cláusula de protección y de la palabra clave readonly garantiza la SodaConsumer invariantes de clase, lo que le permite usar la dependencia sin preocuparse de que sea nula.

+0

Tal vez simplemente entendí mal la pregunta, pero pensé que estaba preguntando cómo implementar el método 'ISodaFactory.Create' - que es donde todas esas instrucciones' switch' o código de reflexión han desaparecido. – Aaronaught

4

Este es el patrón general que utilizo si necesito crear una instancia de la clase correcta basada en un nombre u otros datos serializados:

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(); 
    } 
} 

Tenga en cuenta que todavía tendrá que hacer un montón de ISodaCreator implementaciones, y de alguna manera, recopilar todas las implementaciones para pasar al constructor SodaFactory. Para reducir el trabajo que supone que, se podría crear un único creador de soda genérico basado en la reflexión:

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]) 
    } 
} 

y luego simplemente escanear el conjunto de la ejecución (o alguna otra colección de conjuntos) para encontrar todo tipo de refrescos y construir una SodaFactory así:

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); 

Tenga en cuenta que mi respuesta complementa el de Marcos Seemann: él le está diciendo a permitir que los clientes utilizar un resumen ISodaFactory, por lo que no tienen que preocuparse acerca de cómo obras de fábrica de soda.Mi respuesta es una implementación de ejemplo de una fábrica de refrescos.

3

Creo que el problema no es ni siquiera una fábrica o lo que sea; es tu estructura de clase

¿DrPepper se comporta de manera diferente a Pepsi? ¿Y Coke? Para mí, parece que estás usando interfaces incorrectamente.

Dependiendo de la situación, haría una clase Soda con una propiedad BrandName o similar. El BrandName se configuraría en DrPepper o Coke, ya sea a través de enum o string (la primera parece una mejor solución para su caso).

Resolver el problema de fábrica es fácil. De hecho, no necesitas una fábrica en absoluto. Solo use el constructor de la clase Soda.

+0

Sí, se comportarían de manera diferente. Intenté que mi ejemplo fuera lo más genérico posible. Pero en el mundo real, el método de interfaz "Bebida" se implementaría de manera diferente para cada refresco. – Rune

Cuestiones relacionadas