Enfoques graduales a la inyección de dependencia

12

Estoy trabajando para hacer que mis clases sean comprobables por unidad, usando la inyección de dependencia. Pero algunas de estas clases tienen muchos clientes, y aún no estoy listo para refactorizarlos para comenzar a pasar las dependencias. Entonces estoy tratando de hacerlo gradualmente; manteniendo las dependencias predeterminadas por ahora, pero permitiendo que se anulen para la prueba.

Un enfoque que estoy planteando es simplemente mover todas las llamadas "nuevas" a sus propios métodos, por ejemplo:

public MyObject createMyObject(args) {
  return new MyObject(args);
}

Luego, en mis pruebas unitarias, puedo subclasificar esta clase y anular las funciones de creación, para que creen objetos falsos.

¿Es este un buen enfoque? ¿Hay alguna desventaja?

En términos más generales, ¿está bien tener dependencias codificadas, siempre que pueda reemplazarlas para realizar pruebas? Sé que el enfoque preferido es exigirlos explícitamente en el constructor, y me gustaría llegar allí eventualmente. Pero me pregunto si este es un buen primer paso.

Una desventaja que se me acaba de ocurrir: si tiene subclases reales que necesita probar, no puede reutilizar la subclase de prueba que escribió para la clase principal. Tendrá que crear una subclase de prueba para cada subclase real, y tendrá que anular las mismas funciones de creación.

JW01
fuente
¿Puedes explicar por qué no se pueden probar unidades ahora?
Austin Salonen
Hay muchas "nuevas X" dispersas en todo el código, así como muchos usos de clases estáticas y singletons. Entonces, cuando trato de probar una clase, realmente estoy probando un montón de ellas. Si puedo aislar las dependencias y reemplazarlas con objetos falsos, tendré más control sobre las pruebas.
JW01

Respuestas:

13

Este es un buen enfoque para comenzar. Tenga en cuenta que lo más importante es cubrir su código existente con pruebas unitarias; Una vez que tenga las pruebas, puede refactorizar más libremente para mejorar aún más su diseño.

Entonces, el punto inicial no es hacer que el diseño sea elegante y brillante, solo hacer que la unidad de código sea verificable con los cambios menos riesgosos . Sin pruebas unitarias, debe ser más conservador y cauteloso para evitar descifrar su código. Estos cambios iniciales pueden incluso hacer que el código se vea más torpe o feo en algunos casos, pero si te permiten escribir las primeras pruebas unitarias, eventualmente podrás refactorizar hacia el diseño ideal que tienes en mente.

El trabajo fundamental para leer sobre este tema es trabajar eficazmente con código heredado . Describe el truco que muestra arriba y muchos, muchos más, usando varios idiomas.

Péter Török
fuente
Solo un comentario sobre esto "una vez que tiene las pruebas, puede refactorizar más libremente". Si no tiene pruebas, no está refactorizando, solo está cambiando el código.
ocodo
@Slomojo Entiendo su punto de vista, pero aún puede realizar muchas refactorizaciones seguras sin pruebas, en particular las clasificaciones IDE automatizadas como, por ejemplo (en eclipse para Java) extraer código duplicado a métodos, renombrar todo, mover clases y extraer campos, constantes etc.
Alb
Realmente solo estoy citando los orígenes del término Refactorización, que está modificando el código en patrones mejorados que están protegidos de los errores de regresión mediante un marco de prueba. Por supuesto, la refactorización basada en IDE ha hecho que sea mucho más trivial el software 'refactorizador', pero tiendo a preferir evitar el autoengaño, si mi software no tiene pruebas, y yo "refactorizo" solo estoy cambiando el código. Por supuesto, eso puede no ser lo que le digo a otra persona;)
ocodo
1
@Alb, refactorizar sin pruebas siempre es más arriesgado. Las refactorizaciones automatizadas seguramente disminuyen ese riesgo, pero no a cero. Siempre hay casos extremos complicados que las herramientas pueden no ser capaces de manejar correctamente. En el mejor de los casos, el resultado es un mensaje de error de la herramienta, pero en el peor de los casos puede introducir silenciosamente errores sutiles. Por lo tanto, se recomienda mantener los cambios más simples y seguros posibles incluso con herramientas automatizadas.
Péter Török
3

La gente de Guice recomienda usar

@Inject
public void setX(.... X x) {
   this.x = x;
}

para todas las propiedades en lugar de simplemente agregar @Inject a su definición. Esto le permitirá tratarlos como beans normales, lo que puede hacer newal probar (y configurar las propiedades manualmente) y @Inject en producción.


fuente
3

Cuando me enfrente con este tipo de problema, identificaré cada dependencia de mi clase a prueba y crearé un constructor protegido que me permitirá pasarlos. Esto es efectivamente lo mismo que crear un setter para cada uno, pero prefiero declarando dependencias en mis constructores para que no me olvide de crear instancias en el futuro.

Entonces, mi nueva clase de unidad de prueba tendrá 2 constructores:

public MyClass() { 
  this(new Client1(), new Client2()); 
}

public MyClass(Client1 client1, Client2 client2) { 
  this.client1 = client1; 
  this.client2 = client2; 
}
Seth M.
fuente
2

Es posible que desee considerar la expresión "getter crea" hasta que pueda usar la dependencia inyectada.

Por ejemplo,

public class Example {
  private MyObject myObject=null;

  public MyObject getMyObject() {
    if (myObject==null) {
      myObject=new MyObject();
    } 
    return myObject;
  }

  public void setMyObject(MyObject myObject) {
    this.myObject=myObject;
  }

  private void myMethod() {
    if (getMyObject().doSomething()) {
      // Instance automatically created
    }
  }
}

Esto le permitirá refactorizar internamente para que pueda hacer referencia a su myObject a través del captador. Nunca tienes que llamar new. En el futuro, cuando todos los clientes estén configurados para permitir la inyección de dependencias, todo lo que tiene que hacer es eliminar el código de creación en un lugar: el captador. El setter le permitirá inyectar objetos simulados según sea necesario.

El código anterior es solo un ejemplo y expone directamente el estado interno. Debe considerar cuidadosamente si este enfoque es apropiado para su base de código.

También le recomendaría que lea " Trabajar eficazmente con código heredado ", que contiene una gran cantidad de consejos útiles para tener éxito en una refactorización importante.

Gary Rowe
fuente
Gracias. Estoy considerando este enfoque para algunos casos. Pero, ¿qué haces cuando el constructor de MyObject requiere parámetros que solo están disponibles dentro de tu clase? ¿O cuando necesita crear más de un MyObject durante la vida de su clase? ¿Tienes que inyectar un MyObjectFactory?
JW01
@ JW01 Puede configurar el código creador del captador para leer el estado interno de su clase y simplemente ajustarlo. Crear más de una instancia durante la vida será difícil de organizar. Si te encuentras con esa situación, te sugiero un rediseño, en lugar de una solución alternativa. No hagas que tus captadores sean demasiado complejos o se convertirán en una piedra de molino alrededor de tu cuello. Su objetivo es facilitar el proceso de refactorización. Si parece demasiado difícil, muévase a un área diferente para arreglarlo allí.
Gary Rowe el
Eso es un singleton. Simplemente no use singletons O_O
CoffeDeveloper
@DarioOO En realidad es un singleton muy pobre. Una mejor implementación usaría una enumeración según Josh Bloch. Los Singletons son un patrón de diseño válido y tienen sus usos (creación única costosa, etc.).
Gary Rowe