Introducción a la inyección de dependencias

Imaginad que estáis trabajando en el software de una tienda online y queréis escribir tests de unidad para el módulo de pagos:

public class PaymentService {
	private final BancoPepePlatform pasarela;

	public PaymentService() {
		this.pasarela = new BancoPepePlatform();
	}

	public String cobrar(Money cantidad, String titular, String numero,
			int caducidadMes, int caducidadAño, int cvv) {
		Result res = pasarela.charge(titular, numero, caducidadMes,
				caducidadAño, cvv, cantidad);
		return res.isSuccess() ? res.getCode() : null;
	};
}

Pronto os encontraréis con un problema bastante gordo: cada vez que se ejecute uno de los tests estaréis comunicándoos con el Banco Pepe. Si la conexión es lenta, los tests de unidad tardarán mucho tiempo en ejecutarse; si el banco cobra por cada transacción, ejecutar los tests saldrá muy caro (literalmente); si en el servidor del banco tienen un bug o la conexión falla, puede que los tests fallen sin que sea culpa vuestra; si alguien se olvida de usar un número de tarjeta de pruebas, alguien se enfadará mucho. Y esto sólo para empezar.

Los tests de unidad deberían ser rápidos, deberían probar cada componente de forma aislada, y deberían depender del mínimo posible de infraestructura para evitar introducir errores no debidos al componente que estamos probando. Estas tres cosas son todo lo contrario de lo que he escrito en el párrafo anterior; por lo tanto, tenemos que encontrar una solución.

La solución pasa por emplear en los tests de unidad un “simulador” de BancoPepePlatform. Este “simulador” puede funcionar de muchas maneras distintas; la idea es que sea rápido, fiable, y que exponga la suficiente funcionalidad para poder utilizarlo en los tests de unidad de PaymentService. El problema que tenemos ahora es hacer que PaymentService use el BancoPepePlatform de verdad o el simulador, dependiendo de si está ejecutándose el código de verdad o los tests de unidad.

Podríamos intentar usar, por ejemplo, una factoría estática que consulte la configuración y devuelva una instancia de uno u otro tipo:

	public class BancoPepePlatformFactory {
		public static BancoPepePlatform get() {
			if (SystemConfiguration.isTestMode()) {
				return new FakeBancoPepePlatform();
			} else {
				return new BancoPepePlatformImpl();
			}
		}
	}

	public PaymentService() {
		this.pasarela = BancoPepePlatformFactory.get();
	}

No obstante, esta solución tiene sus propios problemas: tenemos que acordarnos de activar el “modo test” en todos los tests de unidad, tenemos que incluir FakeBancoPepePlatform en los binarios de producción de la tienda online aunque no vamos a utilizarlo para nada, y si en un test determinado queremos utilizar un simulador distinto, no podemos.

Una mejor solución para esto es inyectar la dependencia. Es decir, PaymentService no crea la instancia de BancoPepePlatform que necesita, sino que se le proporciona una:

	public PaymentService(BancoPepePlatform pasarela) {
		this.pasarela = pasarela;
	}

A partir de este momento, cada vez que creéis una instancia de PaymentService tendréis que crear también una instancia de BancoPepePlatform y pasársela en el constructor. Por ejemplo, de esta manera en vuestro código de producción:

PaymentService service = new PaymentService(new BancoPepePlatformImpl());

Y de esta forma en los tests de unidad:

PaymentService service = new PaymentService(new FakeBancoPepePlatform());

Si en un test necesitáis una instancia “especial” de BancoPepePlatform es trivial proporcionársela:

PaymentService service = new PaymentService(new BancoPepePlatform() {
	@Override
	public Result charge(String titular, String numero,
			int caducidadMes, int caducidadAño, int cvv, Money cantidad) {
		return null;
	}
});

Ahora tendremos que ir hacia “arriba” en la cadena de dependencias y seguir aplicando el patrón, porque si no, seguiremos teniendo el mismo problema de antes. Por ejemplo, veamos PaymentServlet:

public class PaymentServlet extends HttpServlet {
	private final PaymentService paymentService;

	public PaymentServlet(PaymentService paymentService) {
		this.paymentService = paymentService;
	}
	
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		// ...
	}
}

Al final llegaremos a un punto en el que tendremos código que crea toda la cadena de dependencias del programa:

servlets.add(new PaymentServlet(new PaymentService(new BancoPepePlatformImpl())));

Esta cadena de dependencias puede ser bastante difícil de mantener. Por eso, prácticamente todo el mundo utiliza un “framework” de inyección de dependencias como Spring o Guice, que es el que mejor conozco.

Estos frameworks proporcionan un “inyector”, que es una función que proporciona una instancia de la clase solicitada, inyectando todas las dependencias intermedias. Por ejemplo, con Guice podría obtener una instancia de PaymentServlet de esta manera:

PaymentServlet servlet = injector.getInstance(PaymentServlet.class);

Para inyectar correctamente las clases es necesario marcar sus puntos de inyección. En Guice, esto se hace utilizando la anotación @Inject:

@Inject
public PaymentServlet(PaymentService paymentService) {
	this.paymentService = paymentService;
}

@Inject
public PaymentService(BancoPepePlatform pasarela) {
	this.pasarela = pasarela;
}

Lo más habitual es utilizar un constructor o una o más funciones “setter” como puntos de inyección. Sólo puede marcarse un constructor, pero pueden marcarse todas las funciones que se quiera.

Finalmente, el inyector tiene una configuración, que le indica qué clases inyectar. Esto es necesario si en algún punto de inyección se hace referencia a un interfaz; Guice necesita saber qué implementación se debe inyectar para ese interfaz. Por ejemplo, para que Guice inyecte una instancia de BancoPepePlatformImpl para el interfaz BancoPepePlatform:

public class PaymentModule extends AbstractModule {
	@Override
	protected void configure() {
		bind(BancoPepePlatform.class).to(BancoPepePlatformImpl.class);
	}
}

Esta configuración se le pasa a Guice al crear el inyector:

Injector injector = Guice.createInjector(new PaymentModule());

Guice también permite hacer muchas cosas en su configuración. Por ejemplo, se le puede indicar que sólo debería existir una instancia de una clase:

bind(PaymentServlet.class).in(Scopes.SINGLETON);

O que el objeto a inyectar tiene que venir de una factoría:

bind(PaymentService.class).toProvider(new PaymentServiceProvider());

O utilizar anotaciones para hacer distintas inyecciones para la misma interfaz:

bind(BancoPepePlatform.class).annotatedWith(Paypal.class).to(PaypalBancoPepePlatform.class);

@Inject
public PaypalPaymentService(@Paypal BancoPepePlatform platform) {
	this.platform = platform;
}

Una pequeña nota antes de terminar: si usáis la inyección de dependencias correctamente, necesitaréis usar directamente el inyector solamente una vez. Con él obtendréis una instancia de ShopServer, por ejemplo, que tendrá inyectados todos los objetos que necesite; éstos, a su vez, tendrán inyectadas también todas sus dependencias, y así sucesivamente.

Os aconsejo aprender más sobre inyección de dependencias y echarle un vistazo a la documentación de Guice, que explica todo esto muy bien, porque este patrón de diseño y estas herramientas ayudan muchísimo a hacer que vuestras aplicaciones sean modulares y que los tests de unidad sean más fáciles de escribir.

(Primer artículo, siguiente artículo).