Inyección de dependencias y patrones – Buenas prácticas de programación

Qué es la inyección de dependencias (DI) y por qué es importante

El acoplamiento entre componentes puede ser un problema en el desarrollo de software, ya que puede dificultar la modificación y el mantenimiento de un sistema. Cuando los componentes están muy acoplados, cualquier cambio en uno de ellos puede tener efectos impredecibles en el resto de la aplicación. En este artículo vamos a ver una solución a este problema utilizando la técnica de Inyección de Dependencias.

Inyección de Dependencias (Dependency Injection, DI): Es un patrón de diseño de software en el que uno o más objetos (dependencias) son pasados a otro objeto (dependiente) en tiempo de ejecución. Esto significa que el objeto dependiente no necesita crear o buscar sus dependencias; en cambio, las dependencias se “inyectan” en el objeto dependiente, típicamente a través de su constructor o métodos setter. El propósito de la inyección de dependencias es para lograr la separación de preocupaciones y facilitar la prueba unitaria.

Inversión de Dependencias (Dependency Inversion Principle, DIP): Es uno de los cinco principios de SOLID en la programación orientada a objetos. DIP establece que las “abstracciones no deben depender de los detalles, los detalles deben depender de las abstracciones”. En otras palabras, en lugar de que los módulos de alto nivel dependan de los módulos de bajo nivel, ambos deberían depender de abstracciones, lo que facilita el mantenimiento y la modificación del código.

Es importante mencionar que estos dos conceptos a menudo van de la mano. La Inversión de Dependencias es un principio general de diseño, mientras que la Inyección de Dependencias es una técnica específica que se puede utilizar para lograr ese principio. En muchas situaciones, utilizarás la Inyección de Dependencias para implementar la Inversión de Dependencias en tu código.

En este artículo, vamos a explorar en profundidad esta técnica y veremos cómo puede ayudarnos a mejorar la modularidad y la mantenibilidad de nuestras aplicaciones. También veremos algunos ejemplos concretos de implementación.

2. Dependencias y acoplamiento.

Qué son las dependencias y por qué el acoplamiento puede ser un problema

En cualquier aplicación de software, los diferentes componentes o módulos suelen depender unos de otros para funcionar correctamente. Estas dependencias pueden ser de diferentes tipos, por ejemplo: una dependencia de código, en la que un componente necesita utilizar una función o clase definida en otro componente, o una dependencia de datos, en la que un componente necesita acceder a información almacenada en otro componente.

El problema con las dependencias es que pueden generar un alto acoplamiento entre los diferentes componentes de nuestra aplicación. El acoplamiento se refiere a la medida en que los diferentes componentes de un sistema dependen entre sí. Cuando los componentes están muy acoplados, cualquier cambio en uno de ellos, como dijimos anteriormente, puede tener efectos impredecibles en el resto de la aplicación.

Por ejemplo, supongamos que tenemos una aplicación que contiene un módulo A que depende de un módulo B para funcionar. Si hacemos cambios en el módulo B, es posible que estos cambios afecten a la forma en que A funciona. Esto puede generar problemas de mantenibilidad y de escalabilidad a medida que la aplicación crece y se vuelve más compleja.

Por otro lado, si los componentes de una aplicación están poco acoplados, es decir, si tienen pocas dependencias entre sí, pueden ser modificados o reemplazados con mayor facilidad sin afectar al resto de la aplicación. Esto puede mejorar la mantenibilidad de la aplicación, y hacerla más escalable y adaptable a cambios futuros.

Por lo tanto, es importante minimizar el acoplamiento entre los diferentes componentes de una aplicación, y la inyección de dependencias es una técnica que nos permite hacerlo. En el siguiente apartado, veremos cómo funciona la inyección de dependencias en Python y cómo puede ayudarnos a reducir el acoplamiento en nuestras aplicaciones.

3. Inyección de dependencias en Python

Cómo funciona la inyección de dependencias en Python

En Python, la inyección de dependencias se puede implementar de varias maneras, pero la idea fundamental es la misma: en lugar de que los componentes de una aplicación creen sus propias dependencias, se les proporcionan desde el exterior. Esto significa que un componente no tiene que preocuparse por cómo se crean sus dependencias, sino simplemente por utilizarlas.

Para ilustrar cómo funciona, vamos a ver un ejemplo sencillo. Supongamos que tenemos una aplicación que consta de dos módulos: cliente.py y proveedor.py. La clase Cliente necesita utilizar una clase Proveedor definida en el módulo proveedor.py. Normalmente, lo que haríamos es importar la clase Proveedor en cliente.py y crear una instancia de ella dentro del código. Por ejemplo:

dependencias

Como podemos observar, la clase Provider está fuertemente acoplada a la clase Client. Si queremos aplicar inyección de dependencias en el anterior ejemplo, tendríamos que pasarle como argumento al constructor de Client una instancia de Provider, quedando de la siguiente manera.

A simple vista no parece que exista mucha diferencia, pero como veremos más adelante en el ejemplo práctico, esta manera de trabajar es mucho más escalable y mantenible que el anterior ejemplo. Cabe destacar que en este ejemplo no se cumple el principio de Inversión de Dependencias. Ya que no estamos dependiendo de ninguna interfaz, si no de una implementación.

4. Beneficios de la inyección de dependencias

Cómo la inyección de dependencias puede mejorar la mantenibilidad del código

A continuación, presentaré algunos de los beneficios más importantes de la inyección de dependencias:

1. Desacoplar las clases.

Con esta técnica se nos permite desacoplar las clases entre sí, lo que significa que una clase no tiene que saber cómo se crean las instancias de sus dependencias. En lugar de eso, la dependencia se proporciona al objeto a través de su constructor, lo que hace que la clase sea más independiente y fácil de mantener.

2. Mejorar la organización del código.

La inyección de dependencias puede mejorar la organización del código al permitir la separación de las responsabilidades de cada clase. Esto significa que las clases pueden ser más pequeñas y tener una única responsabilidad, lo que facilita su mantenimiento y extensión.

3. Hacer las aplicaciones más flexibles.

Esta técnica hace que las aplicaciones sean más flexibles, ya que las dependencias se pueden cambiar fácilmente. Esto significa que, si una dependencia cambia o se actualiza, sólo es necesario actualizar el código de la clase que la utiliza, en lugar de tener que actualizar todas las instancias de la dependencia.

4. Hacer las aplicaciones más fáciles de probar.

La inyección de dependencias puede hacer que las aplicaciones sean más fáciles de probar, ya que las dependencias se pueden reemplazar por mocks durante las pruebas. Esto significa que se puede probar la lógica de la clase de forma aislada, sin tener que preocuparse por las dependencias externas.

5. Fomentar la reutilización del código.

La inyección de dependencias puede fomentar la reutilización del código, ya que las clases se pueden utilizar en diferentes contextos y aplicaciones. Esto significa que se puede escribir una vez y utilizar en varios lugares, lo que reduce el tiempo y el esfuerzo necesarios para desarrollar nuevas aplicaciones.

5. Ejemplo práctico

Un ejemplo concreto de cómo implementar la inyección de dependencias combinado con abstracción, factory y chain- responsibility en Python

Ahora mostraré un ejemplo completo, con un caso de uso posible para ver el potencial de aplicar inyección de dependencias combinado con algunos patrones de programación.

Imaginemos que nos piden realizar un módulo que sea capaz de leer de una base de datos MongoDB un listado de documentos para devolverlos como un diccionario. Hagamos 2 ramas del código, una utilizando Inyección de dependencias y otra sin ella.

Sin inyección de dependencias:

dependencias2

Con inyección de dependencias y abstracción:

inyección de dependencias

En este punto entra en juego la abstracción, es importante depender de interfaces y no de implementaciones.

Ahora imaginemos que queremos escalar la aplicación, ahora queremos que también se lea de una cache, pero queremos que sea configurable, es decir. No siempre vamos a querer leer de una cache, solo en ciertas ocasiones.

Sin inyección de dependencias:

código

Aquí ya podemos empezar a ver algunos problemas de mantenimiento, ya que tenemos que propagar un parámetro de APP a MongoClient y de esta manera estamos juntamos responsabilidades y generamos acoplamiento.

Con inyección de dependencias, abstracción y método factoría:

Code 2

Podemos observar cómo MongoClient no ha cambiado nada. Por otro lado, el constructor de APP tampoco ha cambiad, ya que delegamos la instanciación y la lógica en un método de factoría que nos genera una APP que lee de una base de datos MongoDB con cache de forma opcional.

También hemos separado la responsabilidad de leer de una cache a su propia clase y ya no dependemos de añadir lógica adicional al resto de dependencias (MongoClient). Si es cierto, que puede ser una forma un poco más enrevesada de diseñar código, pero con el siguiente paso veremos su potencial.

Ahora queremos añadir otro cliente al juego, queremos que también podamos leer de un fichero CSV de la misma manera que tenemos con la base de datos.

Sin inyección de dependencias:

Se puede ver claramente, como la lógica de los clientes está fuertemente ligada a la clase APP, por otro lado, el constructor de APP deber recibir tantos parámetros como requieran las clases que tiene que instanciar. Se puede ver como empiezan a aparecer duplicidades de código, código espagueti, etc.

Esta implementación es bastante más complicada de hacer y es propensa a muchos errores. Por otro lado, APP se ha vuelto muy difícil de testear debido a su cantidad de dependencias. Este código es poco mantenible y no es escalable.

Con inyección de dependencias, abstracción, método factoría y chain- responsibility

En este caso no hemos tocado nada en MongoClient, ni CacheReader y lo más importante, no hemos tenido que tocar la lógica de negocio de APP, simplemente hemos tenido que crear una nueva implementación de BaseClient y crear un nuevo método de factoría. El resto sigue igual. Podemos ver como el constructor de APP queda limpio, a cambio tenemos que crear tantos métodos de factoría como dependencias de implementaciones tengamos (aunque esto se puede solucionar añadiendo chain- responsibility al método factoría)

De esta forma se pueden implementar lógicas aparentemente complejas de formas muy sencillas y mantenibles. En este caso, se intenta leer de una base de datos MongoDB, si no encuentra el resultado, lo delega a un lector de ficheros CSV. Todo ello manteniendo la opción de cachear el resultado en ambos casos.

Este código es mantenible y escalable. Por otro lado, es muy sencillo de testear, ya que solo tenemos que mockear un solo cliente.

Para ilustrar mejor la flexibilidad de esta técnica, ahora vamos a utilizar un cliente de MySQL en lugar de un MongoDB. Lo primero es crear una implementación nueva que lea de una base de datos MySQL.

Obviando los métodos factoría que ya tiene APP, podemos usar una conexión de MongoDB, MySQL o CSVReader tan solo cambiando la dependencia que le pasamos al constructor.

Por último, vamos a ver un ejemplo con tests. Veremos como la implementación con DI es mucho más sencilla de testear respecto con la que no usa esta técnica. Vamos a realizar una serie de tests para la clase APP.

Tests sin inyección de dependencias:

En estos tests tenemos un problema principal. Si queremos mockear, primero debemos instanciar la clase APP y mockear sus elementos internamente. Pudiendo tener problemas si el constructor inicial de alguna implementación tiene cierta lógica… Por otro lado, no estamos comprobando el funcionamiento interno de los clientes, si no que mockeamos sus métodos. No podemos parametrizar ningún método ya que existe un fuerte acoplamiento entre las implementaciones y la lógica de negocio de APP.

Tests con inyección de dependencias:

A simple vista se puede apreciar que ahora los tests son mucho más sencillos de seguir. Pudiendo mockear cada implementación por separado, creando “fixtures” y pudiendo parametrizar ciertos tests dependiendo que implementación queramos probar. Por otro lado, en este caso es bastante más sencillo probar el flujo de los métodos de los clientes.

6. Conclusión

En conclusión, la inyección de dependencias en Python combinada con abstracción, el patrón de método de factoría y chain-responsibility es una técnica poderosa para lograr una arquitectura más modular y escalable en nuestras aplicaciones. Esta combinación nos permite desacoplar las diferentes partes de nuestro código, facilitando la gestión de dependencias y permitiendo una mayor flexibilidad y facilidad para hacer cambios en el futuro. Además, al utilizar la cadena de responsabilidad, podemos implementar una lógica de negocio más compleja y flexible, sin comprometer la cohesión de nuestro código. En resumen, el uso de estas técnicas nos ayudará a tener una arquitectura más sólida y escalable en nuestras aplicaciones. Además de hacernos la vida mucho más fácil a la hora de hacer tests unitarios.

Por Antonio Carvajal Sansegundo

+Infosobreserquo