¿La inyección de dependencia del pobre es una buena forma de introducir la capacidad de prueba en una aplicación heredada?

14

El año pasado, creé un nuevo sistema usando Inyección de dependencia y un contenedor de COI. ¡Esto me enseñó mucho sobre DI!

Sin embargo, incluso después de aprender los conceptos y patrones adecuados, considero que es un desafío desacoplar el código e introducir un contenedor IOC en una aplicación heredada. La aplicación es lo suficientemente grande como para que una implementación real sea abrumadora. Incluso si se entendió el valor y se concedió el tiempo. ¿A quién se le concede tiempo para algo como esto?

¡El objetivo, por supuesto, es llevar las pruebas unitarias a la lógica comercial!
Lógica empresarial que se entrelaza con llamadas de base de datos que evitan las pruebas.

He leído los artículos y entiendo los peligros de la inyección de dependencia del pobre como se describe en este artículo de Los Techies . Entiendo que realmente no desacopla nada.
Entiendo que puede involucrar mucha refactorización de todo el sistema ya que las implementaciones requieren nuevas dependencias. No consideraría usarlo en un nuevo proyecto con ninguna cantidad de tamaño.

Pregunta: ¿Está bien usar la DI de Poor Man para introducir la capacidad de prueba en una aplicación heredada y comenzar a rodar?

Además, ¿está utilizando la DI de Poor Man como un enfoque de base para la verdadera inyección de dependencia una forma valiosa de educar sobre la necesidad y los beneficios del principio?

¿Puede refactorizar un método que tiene una dependencia de llamada a la base de datos y abstraer esa llamada detrás de una interfaz? Simplemente tener esa abstracción haría que ese método sea comprobable ya que una implementación simulada podría pasarse a través de una sobrecarga del constructor.

En el futuro, una vez que el esfuerzo gane simpatizantes, el proyecto podría actualizarse para implementar un contenedor de COI y los constructores estarían allí para captar las abstracciones.

Airn5475
fuente
2
Solo una nota. I consider it a challenge to decouple code and introduce an IOC container into a legacy applicationpor supuesto que lo es. Se llama deuda técnica. Es por eso que antes de cualquier renovación importante, es preferible refactores pequeños y continuos. Reducir los principales defectos de diseño y pasar a IoC sería menos difícil.
Laiv

Respuestas:

25

La crítica sobre la inyección de Poor Man en NerdDinner tiene menos que ver con si usas o no un contenedor DI que con la configuración correcta de tus clases.

En el artículo, afirman que

public class SearchController : Controller {

    IDinnerRepository dinnerRepository;

    public SearchController() : this(new DinnerRepository()) { }

    public SearchController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
}

es incorrecto porque, si bien el primer constructor proporciona un mecanismo de respaldo conveniente para construir la clase, también crea una dependencia estrechamente vinculada a DinnerRepository.

El remedio correcto, por supuesto, no es, como sugiere Los Techies, agregar un contenedor DI, sino más bien eliminar al constructor ofensor.

public class SearchController : Controller 
{
    IDinnerRepository dinnerRepository;

    public SearchController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
}

La clase restante ahora tiene sus dependencias correctamente invertidas. Ahora eres libre de inyectar esas dependencias como quieras.

Robert Harvey
fuente
Gracias por tus pensamientos! Entiendo el "constructor ofensor" y cómo debemos evitarlo. Entiendo que tener esta dependencia no desacopla nada, pero sí permite pruebas unitarias. Estoy en la etapa inicial de introducir la DI y obtener las pruebas unitarias en su lugar lo más rápido posible. Estoy tratando de evitar la complicación de un contenedor o fábrica DI / IOC tan temprano en el juego. Como se mencionó, más tarde, cuando crezca el soporte para DI, podríamos implementar un Contenedor y eliminar esos "constructores ofensivos".
Airn5475
El constructor predeterminado no es necesario para las pruebas unitarias. Puede entregar cualquier dependencia que desee a la clase sin ella, incluido un objeto simulado.
Robert Harvey
2
@ Airn5475 tener cuidado de que no está sobre abstrayendo también. Esas pruebas deben probar comportamientos significativos: probar que un XServicepasa un parámetro a un XRepositoryy devuelve lo que devuelve no es demasiado útil para nadie y tampoco le da mucho en cuanto a la extensibilidad. Escriba sus pruebas unitarias para probar un comportamiento significativo y asegúrese de que sus componentes no sean tan estrechos que realmente esté cambiando toda su lógica a la raíz de su composición.
Ant P
No entiendo cómo incluir al constructor infractor ayudaría para las pruebas unitarias, ¿seguramente hace lo contrario? Elimine el constructor ofensivo para que la DI se implemente correctamente y pase una dependencia simulada al instanciar los objetos.
Rob
@Rob: no lo hace. Es solo una forma conveniente para que el constructor predeterminado defienda un objeto válido.
Robert Harvey
16

Aquí haces una suposición falsa sobre lo que es la "DI del pobre".

Crear una clase que tenga un constructor de "atajo" que todavía cree acoplamiento no es la DI del pobre.

No usar un contenedor, y crear todas las inyecciones y mapeos manualmente, es la DI del pobre.

El término "DI del pobre" suena como algo malo. Por esa razón, el término "DI pura" se recomienda en estos días, ya que suena más positivo y en realidad describe con mayor precisión el proceso.

No solo es absolutamente correcto usar la DI pura / pobre del pobre para introducir la DI en una aplicación existente, sino que también es una forma válida de usar la DI para muchas aplicaciones nuevas. Y como usted dice, todos deberían usar DI puro en al menos un proyecto para comprender realmente cómo funciona DI, antes de asumir la responsabilidad de la "magia" de un contenedor de IoC.

David Arno
fuente
8
La DI "pobre" o "pura", lo que prefiera, también mitiga muchos de los problemas con los contenedores de IoC. Solía ​​usar contenedores IoC para todo, pero cuanto más tiempo pasa, más prefiero evitarlos por completo.
Ant P
77
@AntP, estoy contigo: estoy 100% puro DI en estos días y evito activamente los contenedores. Pero yo (nosotros) somos una minoría en ese puntaje por lo que puedo decir.
David Arno el
2
Parece muy triste: a medida que me alejo del campamento "mágico" de contenedores de IoC, burlándome de las bibliotecas, etc., y me encuentro abrazando OOP, encapsulación, dobles de prueba enrollados a mano y verificación de estado, parece que la tendencia de la magia todavía está en el arriba. +1 de todos modos.
Ant P
3
@AntP & David: Es bueno saber que no estoy solo en el campamento 'Pure DI'. Los contenedores DI se crearon para abordar las deficiencias en los idiomas. Añaden valor en ese sentido. Pero en mi opinión, la verdadera respuesta es arreglar los idiomas (o pasar a otros idiomas) y no construir cruft encima de ellos.
JimmyJames
1

Los cambios de paragidm en equipos / bases de código heredados son extremadamente riesgosos:

En cualquier momento en que usted sugiere "mejoras" a código heredado y hay un "legado" programadores en el equipo, que están diciendo a todos que "lo hicieron mal" y te conviertes en el enemigo para el resto de su / su tiempo con la empresa.

Usar un DI Framework como un martillo para romper todas las uñas solo hará que el código heredado sea peor que mejor en todos los casos. También es extremadamente arriesgado personalmente.

Incluso en los casos más limitados, como solo para que pueda usarse en casos de prueba, solo hará que esos casos de prueba sean "no estándar" y el código "extranjero" que, en el mejor de los casos, solo se marcarán @Ignorecuando se rompan o, peor aún, se quejarán constantemente Los programadores heredados con la mayor influencia en la administración y se reescriben "correctamente" con todo este "tiempo perdido en las pruebas unitarias", atribuido únicamente a usted.

La introducción de un marco DI, o incluso el concepto de "DI puro" en una aplicación de línea de negocios, y mucho menos una enorme base de código heredado sin la ayuda de la administración, el equipo y especialmente el patrocinio del desarrollador principal serán la sentencia de muerte para usted social y políticamente en el equipo / empresa. Hacer cosas como esta es extremadamente arriesgado y puede ser un suicidio político de la peor manera.

La inyección de dependencia es una solución que busca un problema.

Cualquier lenguaje con constructores, por definición, utiliza la inyección de dependencia por convención si sabe lo que está haciendo y entiende cómo usar un constructor correctamente, este es un buen diseño.

La inyección de dependencia solo es útil en pequeñas dosis para un rango muy limitado de cosas:

  • Cosas que cambian mucho o tienen muchas implementaciones alternativas que están vinculadas estáticamente.

    • Los controladores JDBC son un ejemplo perfecto.
    • Clientes HTTP que pueden variar de una plataforma a otra.
    • Sistemas de registro que varían según la plataforma.
  • Sistemas de complementos que tienen complementos configurables que se pueden definir en el código de configuración en su marco y se descubren automáticamente al inicio y se cargan / recargan dinámicamente mientras se ejecuta el programa.


fuente
10
La aplicación asesina para DI es la prueba unitaria. Como ha señalado, la mayoría del software no necesitará mucha DI por sí sola. Pero las pruebas también son un cliente de este código, por lo que debemos diseñar para la comprobabilidad. En particular, esto significa colocar costuras en nuestro diseño donde las pruebas pueden observar el comportamiento. Esto no requiere un marco DI elegante, pero sí requiere que las dependencias relevantes se hagan explícitas y sustituibles.
amon
44
Si los desarrolladores supiéramos si avanza lo que podría cambiar, eso sería la mitad de la batalla. He estado en desarrollos largos donde muchas cosas supuestamente "arregladas" cambiaron. Las pruebas futuras aquí y allá con DI no son malas. Usarlo en todas partes todo el tiempo huele a programación de culto de carga.
Robbie Dee
complicar demasiado la prueba solo significará que quedan obsoletos y marcados como ignorados en lugar de actualizarse en el futuro. DI en el código heredado es una economía falsa. Todo lo que ellos harán es convertirte en el "chico malo" por poner todo este "código no estándar, demasiado complejo que nadie entiende" en la base del código. Usted ha sido advertido.
44
@JarrodRoberson, ese es realmente un ambiente tóxico. Una de las señales clave de un buen desarrollador es que miran hacia atrás al código que escribieron en el pasado y piensan, "uf, ¿realmente escribí eso? ¡Eso está muy mal!" Eso es porque han crecido como desarrolladores y aprendieron cosas nuevas. Alguien que mira el código que escribieron hace cinco años y no ve nada de malo, no ha aprendido nada en esos cinco años. Entonces, si decirle a la gente que lo hicieron mal en el pasado te convierte en un enemigo, huye de esa compañía rápidamente, ya que son un equipo sin salida que te detendrá y te reducirá a su nivel pobre.
David Arno el
1
No estoy seguro de estar de acuerdo con lo malo, pero para mí la conclusión clave de esto es que no necesita DI para hacer pruebas unitarias. Implementar DI para hacer que el código sea más comprobable es solo parchear el problema.
Ant P