Pregunta ¿Una alternativa viable a la inyección de dependencia?


No me gusta la inyección de dependencia basada en el constructor.

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 extensivamente en un proyecto de MVC 3 C # en el último año, encuentro 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 implementació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 menor, y las herramientas automáticas como el reafilado ayudan, pero es ciertamente molesto cuando un simple cambio 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, aumentando la complejidad del código.

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

Crear una instancia de esta clase si solo es posible si estoy dentro de un contexto donde los servicios dados están disponibles y se han resuelto automáticamente (por ejemplo, el controlador) o, infelizmente, pasando la instancia de servicio por múltiples cadenas de clases auxiliares.

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 esté claro, de lo que estoy hablando es de pasar instancias de interfaz DI resueltas a través de múltiples capas desde Sin razón aparte de en la capa inferior, se necesitan inyectar en algo.

Si una clase no depende de un servicio, debe tener una dependencia en ese servicio. Esta idea de que hay una dependencia 'transitoria' donde una clase no utilizar un servicio pero simplemente lo pasa es, en mi opinión, una tontería.

Sin embargo, no sé de una mejor solución.

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

He contemplado utilizar el marco DI dentro de los constructores, ya que esto resuelve algunos de los problemas:

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

¿Hay algún inconveniente para hacer esto?

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

NÓTESE BIEN. 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.


32
2018-02-06 04:06


origen


Respuestas:


En mi opinión, la causa raíz de todos los problemas no es hacer 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).

Deje pasar por sus declaraciones:

Todas las pruebas dependen de los constructores.

[recorte]

Problema: si necesito otro servicio en mi implementació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 de negocios. 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 ninguna news en la lógica de negocios. 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 utilizar el marco DI dentro de los constructores, ya que esto resuelve algunos de los problemas:

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

¿Hay algún inconveniente para hacer esto?

Como beneficio obtendrá todos los inconvenientes de usar el Singleton patrón (código no comprobable y un gran espacio de estado de su aplicación).

Entonces diría que DI debería hacerse bien (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.


18
2018-02-06 04:27



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 que las pruebas unitarias son estrechamente acoplado a los constructores Esto a menudo se puede remediar con un simple SUT Factory - un concepto que puede extenderse a un Contenedor de auto-burla.

En cualquier caso, al usar Constructor Injection, los constructores deberían ser simples así 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 pasan innecesariamente, aumentando la complejidad del código.

De acuerdo, este es ciertamente 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 Servicio de fachada Está perdido. En lugar 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 localizador de servicios, y solo tiene desventajas y no tiene beneficios.


14
2018-02-06 11:40