Uno de los principales problemas al desarrollar software siempre son los tests, muchas veces esto se convierte en una deuda técnica o cuando se realizan los test se vuelven muy complejos de escribir, leer, entender e incluso mantener, una de las principales razones son sus dependencias externas, esto generalmente se traduce en tener mocks muy complejos y algunos incluso podrían considerar esto como code smell, en este contexto es donde entra en juego la inyección de dependencias Ver Figura 1, el concepto no están fácil de abordar, por eso este articulo se dividirá en dos partes, la primera enfocada a las dependencias y la segunda a tests. Iniciando con este concepto de inyección de dependencias, consiste en pensar en las librerías que se necesitan como abstracciones y en tiempo de ejecución enviársela a cada función o método para que lo llame e implemente su lógica, esto va muy de la mano con orientación a objetos, pero no quiere decir que en programación funcional no se pueda hacer, en python tenemos una singularidad y es que python es un lenguaje dinámico, es decir no tiene tipo de datos, pero nos podemos valer de los typing para construir nuestra entrada de dependencias, para ellos utilizaremos un framework llamado dependency injector
Utilizando el framework dependecy injector
Este es un framework que nos facilita la utilización de inyección de dependencias en python, este framework se soporta sobre varios principios, los cuales destaco los siguientes:
- Providers estos son los ensambladores o creadores de instancias y encargados de inyectarlos, son los que contienen las referencias y configuración de lo que se inyectara
- Typing los provider son mypy-friendly es decir a pesar de que la configuración se encapsula en ensambladores, los editores de código reconocen que tipos de datos se están trabajando
- Containers son gestores de providers, al tener muchos providers puede existir la necesidad de agruparlos para que su gestion sea mas sencilla, los containers ayudan a sincronizarlos
- Perfomance este framework esta escrito en ctyhon lo que permite agregar un plus en el rendimiento
Antes de iniciar se debe instalar injector, yo utilizando poetry lo hago muy sencillo con el siguiente comando
poetry add dependency-injector
con estos conceptos claros, podemos pasar al código, voy a crear 6 archivos llamados domain.py, repository.py, infraestructure.py, injection.py, main.py y config.ini los cuales escribo a continuación
- Domain aquí se tiene la clase del dominio en este caso seria una clase solamente con atributos que representan un documento
- Repository este modulo contiene el código abstracto que usaremos para las implementaciones según nuestra necesidad, en este caso definí dos abstracciones, uno para la base de datos y otro para el almacenamiento de datos
- Infrastructure este contiene la implementacion especifica para las abstracciones que anteriormente cree, para ello creo un constructor que recibe como parámetro el cliente especifico para cada implementacion, para el caso de SQLite es una instancia de Connection
- Injection aquí ocurre la magia, en este modulo es donde realmente se realizan las inyecciones, la clase container como mencione arriba, gestiona los providers, para ellos los tenemos de diferentes tipos, en el caso de la base de datos como necesitamos solo una conexión usamos un provider de tipo Singleton y como parametro a la instancia recibe un dsn que cargamos anteriormente de otro provider de tipo config, este provider permite gestionar datos de configuración de los archivos .ini y ademas permite referenciar datos de tipo clases, una vez declarados estos provider, se instancia los de tipo Factory, que trabajan con cualquier tipo de clases y aquí reciben como parámetro los providers que ya habia inicado arriba, esto por lo siguiente
provider1()
│
├──> provider2()
│
├──> provider3()
│ │
│ └──> provider4()
│
└──> provider5()
│
└──> provider6()
como ven los providers se pueden referenciar entre si y el framework realiza el trabajo de instanciar y traducir todo esto a objetos
- main aquí se invoca el código a ejecutar, lo interesante aquí es el @injector que es un decorector que permite indicar en el metodo que recibira la injeccion de dependencias, aqui por cada parametro se llama al container, por ello las siguientes lineas de código son muy importantes
container = Container()
container.init_resources()
container.wire(modules=[__name__])
estas lineas lo que hacen es inicializar todos los objetos necesarios o requeridos en las dependencias, por lo tanto cuando se cargue la aplicación estos quedan en memoria, esto implica que probablemente la aplicación tenga un inicio lento, esto varia en función a la cantidad de objetos que se carguen, pero al ventaja a largo plazo es que se tienen los objetos listos para usarse, convirtiendo esto es una "inversión", teniendo un inicio lento pero durante el tiempo de vida de la aplicación, se tendrá acceso rápido a los objetos necesarios, por lo tanto es de vital importancia que la inicializacion este junto al punto de entrada del proyecto
Conclusiones
Trabajar con inyección de dependencias permite tener un código menos acoplado y con mayor cohesión Ver Figura 2, separando de esta manera responsabilidades, trayendo una alta reutilizacion de codigo, creando un efecto positivo a la hora de escribir test y agregando una mayor agilidad al proceso de parametrizacion, debido a que injector lo gestiona de una forma eficiente, para comprobar que los test son mas sencillos lo veremos en un siguiente articulo