2012-02-06 12 views
30

No me gusta la inyección de dependencia basada en el constructor.¿Una alternativa viable a la inyección de dependencia?

Creo que aumenta la complejidad del código y disminuye el mantenimiento, y me gustaría saber si hay alguna alternativa viable.

No estoy hablando del concepto de separar la implementación de la interfaz, y tener una forma de resolver dinámicamente (recursivamente) un conjunto de objetos desde una interfaz. Yo apoyo completamente eso. Sin embargo, la forma tradicional de hacer esto basada en el constructor parece tener algunos problemas.

1) Todas las pruebas dependen de los constructores.

Después de usar DI ampliamente en un # proyecto MVC 3 C en el último año, puedo encontrar nuestro código lleno de cosas como esta:

public interface IMyClass { 
    ... 
} 

public class MyClass : IMyClass { 

    public MyClass(ILogService log, IDataService data, IBlahService blah) { 
    _log = log; 
    _blah = blah; 
    _data = data; 
    } 

    ... 
} 

Problema: Si necesito otro servicio en mi aplicación tengo que modificar el constructor y eso significa que todas las pruebas unitarias para esta clase se rompen.

Incluso las pruebas que no tienen nada que ver con la nueva funcionalidad requieren al menos refactorización para agregar parámetros adicionales e inyectar un simulacro para ese argumento.

Esto parece ser un problema pequeño, y las herramientas automatizadas como el reafilado ayudan, pero ciertamente es molesto cuando un cambio simple como este hace que se rompan más de 100 pruebas. En términos prácticos, he visto a personas haciendo tonterías para evitar cambiar el constructor en lugar de morder la bala y corregir todas las pruebas cuando esto sucede.

2) Las instancias de servicio se transfieren innecesariamente, lo que aumenta la complejidad del código.

public class MyWorker { 
    public MyWorker(int x, IMyClass service, ILogService logs) { 
    ...  
    } 
} 

Creación de una instancia de esta clase si sólo es posible si estoy dentro de un contexto en que los servicios prestados están disponibles y automáticamente se han resuelto (por ejemplo. Controlador) o, por desgracia, pasando la instancia de servicio por varias cadenas de clases de ayuda.

veo código como este todo el tiempo:

public class BlahHelper { 

    // Keep so we can create objects later 
    var _service = null; 

    public BlahHelper(IMyClass service) { 
    _service = service; 
    } 

    public void DoSomething() { 
    var worker = new SpecialPurposeWorker("flag", 100, service); 
    var status = worker.DoSomethingElse(); 
    ... 

    } 
} 

En caso de que el ejemplo no es clara, lo que estoy hablando es de pasar instancias de interfaz DI resueltos a través de múltiples capas de ninguna razón que no sea en la capa inferior que se necesitan para inyectar en algo.

Si una clase no depende de un servicio, debe tener una dependencia en ese servicio. Esta idea de que existe una dependencia "transitoria" en la que una clase no usa un servicio , sino que simplemente la transfiere es, en mi opinión, una tontería.

Sin embargo, no conozco una solución mejor.

¿Hay algo que proporcione los beneficios de DI sin estos problemas?

he contemplado el uso del marco DI dentro de los constructores en su lugar ya que esto resuelve un par de problemas:

public MyClass() { 
    _log = Register.Get().Resolve<ILogService>(); 
    _blah = Register.Get().Resolve<IBlahService>(); 
    _data = Register.Get().Resolve<IDataService>(); 
} 

¿Hay alguna desventaja de hacer esto?

Significa que las pruebas unitarias deben tener "conocimiento previo" de la clase para unir mocks para los tipos correctos durante el inicio de la prueba, pero no puedo ver ningún otro inconveniente.

NB. Mis ejemplos están en C#, pero también me he topado con los mismos problemas en otros idiomas, y especialmente los idiomas con soporte de herramientas menos maduros, estos son grandes dolores de cabeza.

+0

si lo coloca dentro del constructor crea una dependencia en el componente di usado. si pasas las interfaces al constructor puedes cambiar el componente di, digamos Autofac con NInject o Castle, etc. Así que si realmente estás realmente seguro de que nunca cambiarás el componente di usado y quédate encendido digamos Autofac, esta es una solución muy viable. –

Respuesta

17

En mi opinión, la causa de todos los problemas no está haciendo DI correctamente. El objetivo principal de usar el constructor DI es establecer claramente todas las dependencias de alguna clase. Si algo depende de algo, siempre tiene dos opciones: hacer explícita esta dependencia u ocultarla en algún mecanismo (de esta manera, tiende a generar más dolores de cabeza que beneficios).

Vamos a ir a través de sus declaraciones:

Todas las pruebas dependen de los constructores.

[snip]

Problema: Si necesita otro servicio en mi aplicación tengo que modificar el constructor; y eso significa que todas las pruebas unitarias para esta clase se rompen.

Hacer que una clase dependa de algún otro servicio es un cambio bastante importante. Si tiene varios servicios que implementan la misma funcionalidad, diría que hay un problema de diseño. Las pruebas de burla y realización correctas satisfacen a SRP (que en términos de pruebas unitarias se reduce a "Escribir una prueba por separado para cada caso de prueba") e independiente debería resolver este problema.

2) Las instancias de servicio se transfieren innecesariamente, aumentando la complejidad del código.

Uno de los usos más generales de la DI es separar la creación de objetos de la lógica comercial. En su caso, vemos que lo que realmente necesita es crear un Trabajador que a su vez necesite varias dependencias que se inyecten a través del gráfico completo del objeto. El mejor enfoque para resolver este problema es nunca hacer ningún new s en la lógica comercial. Para tales casos, preferiría inyectar una fábrica de trabajadores que resuma el código comercial de la creación real del trabajador.

he contemplado el uso del marco DI dentro de los constructores en su lugar ya que esto resuelve un par de problemas:

public MyClass() { 
    _log = Register.Get().Resolve<ILogService>(); 
    _blah = Register.Get().Resolve<IBlahService>(); 
    _data = Register.Get().Resolve<IDataService>(); 
} 

¿Hay alguna desventaja de hacer esto?

Como beneficio obtendrá todos los negativos del uso del patrón de Singleton (código no comprobable y un enorme espacio de estado de su solicitud).

Así que yo diría que DI debería hacerse correctamente (como cualquier otra herramienta). La solución a sus problemas (IMO) radica en la comprensión de DI y la educación de los miembros de su equipo.

+4

'Register.Get(). Resolve <>()' es un ejemplo perfecto de [ServiceLocator anti-pattern] (http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx). Si agregar una dependencia más a un servicio da como resultado la ruptura de muchos casos de prueba, debe refactorizar sus pruebas. Como @Alex sugirió hacer que sigan a SRP, es una cosa. Otro sería usar algo como [TestInitializeAttribute] (http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.testtools.unittesting.testinitializeattribute.aspx) (si está utilizando MsTest) para poner el configuración principal de sus pruebas en un solo lugar. –

+0

Útil. "Sé más educado". ¿Cómo es esta la respuesta aceptada? –

13

Es tentador culpar a Constructor Injection por estos problemas, pero en realidad son síntomas de implementación incorrecta más que desventajas de Constructor Injection.

Miremos cada problema aparente individualmente.

Todas las pruebas dependen de los constructores.

El problema aquí es realmente que las pruebas unitarias son fuertemente acoplado a los constructores. Esto a menudo puede remediarse con un simple SUT Factory, un concepto que puede extenderse a Auto-mocking Container.

En cualquier caso cuando se usa Constructor Injection, constructors should be simple, por lo que no hay ninguna razón para probarlos directamente. Son detalles de implementación que ocurren como efectos secundarios de las pruebas de comportamiento que usted escribe.

Las instancias de servicio se transfieren innecesariamente, aumentando la complejidad del código.

De acuerdo, este es sin duda un olor a código, pero una vez más, el olor está en la implementación. No se puede culpar a Constructor Injection por esto.

Cuando esto sucede, es un síntoma que falta Facade Service. En vez de hacer esto:

public class BlahHelper { 

    // Keep so we can create objects later 
    var _service = null; 

    public BlahHelper(IMyClass service) { 
     _service = service; 
    } 

    public void DoSomething() { 
     var worker = new SpecialPurposeWorker("flag", 100, service); 
     var status = worker.DoSomethingElse(); 
     // ... 

    } 
} 

hacer esto:

public class BlahHelper { 
    private readonly ISpecialPurposeWorkerFactory factory; 

    public BlahHelper(ISpecialPurposeWorkerFactory factory) { 
     this.factory = factory; 
    } 

    public void DoSomething() { 
     var worker = this.factory.Create("flag", 100); 
     var status = worker.DoSomethingElse(); 
     // ... 

    } 
} 

En cuanto a la solución propuesta

La solución propuesta es un Servicio de localización, y it has only disadvantages and no benefits.

+0

y ¿qué pasa con el servicio de registro? es un problema que se está inyectando en todas las clases ... – danfromisrael

+2

El registro no es un servicio, es una preocupación transversal: http://stackoverflow.com/questions/7905110/logging-aspect-oriented-programming-and-dependency -injection-trying-to-make/7906547 # 7906547 –

+0

Me gustan sus soluciones, pero parece abogar por el uso del patrón de fábrica en lugar de DI; Estoy de acuerdo con eso, pero ¿podrías explicarme mejor por qué? Hay aspectos negativos (por ejemplo, cubiertos aquí: http://code.google.com/p/google-guice/wiki/Motivation) con un patrón de fábrica que las personas han cubierto anteriormente. – Doug

Cuestiones relacionadas