Inyección de dependencias¶
Introducción¶
La inyección de dependencias es un patrón de diseño que permite a los objetos recibir sus dependencias de manera externa, en lugar de crearlas por sí mismos. Este enfoque facilita la gestión de componentes y promueve un código más modular y mantenible.
Para qué sirve¶
- Simplifica las pruebas unitarias.
- Facilita el mantenimiento y la escalabilidad.
- Reduce el acoplamiento entre componentes.
Importancia¶
- Mejora la flexibilidad del código.
- Permite una mayor reutilización de componentes.
- Facilita la inyección de diferentes implementaciones o configuraciones en tiempo de ejecución.
Tipos¶
En el contexto de la inyección de dependencias, existen tres tipos principales de dependencias: Singleton
, Transient
y Scoped
. Cada una se diferencia en cómo y cuándo se crean las instancias de los servicios a lo largo del ciclo de vida de la aplicación. Estas distintas formas de gestionar las dependencias permiten un mejor control sobre el uso de recursos, la eficiencia y la coherencia del estado en diferentes contextos de la aplicación.
Singleton¶
Descripción: Una única instancia de la dependencia es creada y compartida durante toda la vida de la aplicación.
Casos de Uso: Utilizado para servicios de configuración, conexiones de base de datos o cualquier recurso compartido.
Scoped¶
Descripción: Una única instancia de la dependencia es creada y compartida durante el ciclo de vida de una solicitud (request).
Casos de Uso: Adecuado para servicios que mantienen estado y necesitan ser consistentes durante una solicitud HTTP.
Transient¶
Descripción: Una nueva instancia de la dependencia es creada cada vez que se solicita.
Casos de Uso: Ideal para servicios ligeros que no mantienen estado y son de corta duración.
sequenceDiagram
participant Client
participant Server
participant Service
participant Singleton_Instance
participant Scoped_Instance
participant Transient_Instance
Client->>Server: Solicitud
Server->>Service: Llamar servicio
Service->>Singleton_Instance: Verificar/Crear instancia
Singleton_Instance-->>Service: Proveer instancia
Note left of Singleton_Instance: La instancia no se volverá<br>a crear nunca más.
Service->>Scoped_Instance: Verificar / Crear instancia (Por solicitud)
Scoped_Instance-->>Service: Proveer instancia
Note left of Scoped_Instance: La instancia no se vuelve<br>a crear durante esta solicitud<br>pero creará una nueva en la<br>siguiente solicitud.
Service->>Transient_Instance: Crear nueva instancia (Cada vez)
Transient_Instance-->>Service: Proveer instancia
Note left of Transient_Instance: La instancia se volverá a<br>crear cada vez que se inyecte<br>en esta solicitud y en las<br>siguientes.
Service-->>Server: Retornar instancia
Server-->>Client: Respuesta
Integración¶
Existen tres métodos para almacenar las dependencias según sea el caso: add_singleton
, add_scoped
, add_transient
.
Python | |
---|---|
1 2 3 4 5 6 7 |
|
En el contexto de la inyección de dependencias, existen dos enfoques principales: basadas en interfaces
o en clases
. A continuación, se explica cada uno y se detalla cuándo es más apropiado utilizar uno u otro.
Inyección de Dependencias Basada en Interfaces¶
Este enfoque implica definir una interfaz y asociarla con una clase concreta. Aunque Python no tiene interfaces como tal, es posible utilizar clases abstractas con el módulo ABC
(Abstract Base Classes) para lograr una funcionalidad similar.
Cuándo Usar:
- Flexibilidad: Permite cambiar fácilmente la implementación de la interfaz sin modificar el código dependiente.
- Pruebas Unitarias: Facilita la creación de mock objects para pruebas.
- Desacoplamiento: Reduce la dependencia directa entre componentes, mejorando la mantenibilidad del código.
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
-
Warning
Servicio simulado para el ejemplo. -
Warning
Servicio simulado para el ejemplo.
En el ejemplo anterior, se almacena la dependencia utilizando AUserService
y UserService
.
¿Por qué es esto útil?¶
Supongamos que, más adelante en el proyecto, se requiere no almacenar los usuarios en una base de datos, sino enviarlos a un microservicio. En ese caso, podemos crear otra clase llamada UserServiceToService
con los mismos métodos, sin necesidad de eliminar la clase actual UserService
. Esto implica que no será necesario cambiar, en cada endpoint, la llamada al servicio. Además, nos permite conservar la clase original UserService
en caso de llegar a ser requerida en el futuro.
Este enfoque permite:
- Flexibilidad: Cambiar la implementación de los servicios sin afectar el código dependiente.
- Desacoplamiento: Mantener una separación clara entre la definición de la interfaz y su implementación, lo que mejora la mantenibilidad.
- Reutilización: Permitir que múltiples implementaciones coexistan y se utilicen en diferentes contextos según sea necesario.
Ahora es importante saber cómo inyectar la dependencia, y para esto es indispensable no usar UserService
directamente, sino AUserService
, ya que es a través de esta interfaz como se llegará a UserService
. Esto garantiza que el sistema pueda cambiar fácilmente la implementación de AUserService
sin afectar el código dependiente, permitiendo una mayor flexibilidad y desacoplamiento en tu aplicación.
Inyección de Dependencias Basada en Clases¶
Este enfoque implica inyectar directamente una clase concreta sin utilizar interfaces o clases abstractas. Es menos flexible pero puede ser adecuado para implementaciones simples y directas.
Cuándo Usar:
- Simplicidad: Adecuado para proyectos pequeños o cuando no se espera que la implementación cambie.
- Menos Sobrecarga: No necesitas definir clases abstractas o interfaces, lo que simplifica el desarrollo.
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
-
Warning
Servicio simulado para el ejemplo. -
Warning
Servicio simulado para el ejemplo.
En este caso, a diferencia del ejemplo anterior, se utiliza únicamente CacheUserService
. Esto se debe a que, durante la definición del proyecto, se estimó que las probabilidades de que este servicio cambie son casi nulas. Por lo tanto, este enfoque se consideró el más adecuado para este caso particular.
Para inyectar esta dependencia, es necesario usar CacheUserService
directamente, ya que no se cuenta con una interfaz o clase abstracta asociada a esta implementación.
Comparación y Uso en Python¶
En Python, dado que no existen interfaces formales como en otros lenguajes, es posible utilizar clases abstractas del módulo ABC para lograr una funcionalidad similar. Aquí tienes una comparación general:
Inyección Basada en Interfaces (ABC):
- Pros: Flexibilidad, facilidad para pruebas unitarias, menor acoplamiento.
- Contras: Puede agregar complejidad adicional.
Inyección Basada en Clases:
- Pros: Simplicidad, desarrollo más rápido.
- Contras: Menor flexibilidad, más difícil de cambiar o probar.
Generador (Generator)¶
El generador es otro parámetro (Opcional) al registrar una dependencia. Su función es brindar mayor control sobre cómo se crea y almacena dicha dependencia. Es un Callable
que recibe como parámetro los datos necesarios para la dependencia registrada (**data
).
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
La clase abstracta no es obligatoria, por lo que el código también podría simplificarse de esta forma:
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
Esto ofrece una mayor flexibilidad en la inyección de dependencias, ya que el generador permite no solo personalizar la creación de la dependencia, sino también realizar cualquier acción adicional según sea necesario.
Es importante tener en cuenta que el generador será invocado dependiendo del ciclo de vida de la dependencia (Singleton
, Scoped
, Transient
):
- Singleton: El generador se invocará solo una vez durante todo el proyecto.
- Scoped: Se ejecutará una vez por cada solicitud.
- Transient: Será invocado cada vez que se necesite inyectar la dependencia.
Sobreescribir (override)¶
Determinar si el registro de la dependencia es una sobreescritura requiere un parámetro adicional, el cual sirve para reemplazar una dependencia ya registrada. Este parámetro debe ser pasado como un argumento por palabra clave, es decir, debe referenciarse explícitamente usando el nombre del parámetro.
Python | |
---|---|
1 2 3 |
|
Remover dependencias¶
Para remover una dependencia ya existente se puede usar el método remove_<type>
.
Python | |
---|---|
1 2 3 |
|
Inyección de las dependencias¶
Las dependencias pueden ser inyectadas en varios lugares, desde el controlador, los métodos de validación de los DTO y otras dependencias. Para que esto sea posible es muy importante haberlas almacenado previamente con alguno de los métodos asociados a este add_singleton
, add_scoped
o add_transient
.
En el caso de los controladores se inyectarán directamente en la acción.
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
-
Warning
Servicio simulado para el ejemplo.
En el caso de los validadores de los DTO se usan en cada uno de estos.
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
-
Warning
Servicio simulado para el ejemplo. -
Warning
Servicio simulado para el ejemplo.
En el caso de las otras dependencias estas se inyectan desde el constructor de la clase.
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
-
Warning
Servicio simulado para el ejemplo. -
Warning
Servicio simulado para el ejemplo.
Es importante aclarar que, una vez se inicia con la primera dependencia, esta creará un sistema recursivo donde se analizará cada dependencia para poder inyectar de forma correcta cada una de ellas donde se necesite.
Información importante¶
Las dependencias de tipo Singleton
, al ser permanentes, se comparten entre los diferentes usuarios que ingresan al sistema en cada solicitud. Esto significa que la información almacenada en estas debe ser, preferiblemente, información que no esté relacionada con la sesión del usuario y que sea inmutable.
¿Por qué es importante esto?¶
-
Si se almacena información relacionada con la sesión del usuario, esta podría solaparse entre solicitudes diferentes, generando un agujero de seguridad que podría permitir el acceso no autorizado a determinados servicios.
-
Si la información es mutable (independientemente del tipo), podría sobrescribirse de manera insegura entre solicitudes concurrentes. Esto es lo que se conoce como
Condiciones de carrera (Race Conditions)
. Aunque existe una forma de prevenirlo mediante el uso de bloqueos temporales del recurso, no se recomienda hacerlo para evitar generar cuellos de botella, ya que el bloqueo puede afectar el rendimiento general del sistema.
Ejemplo¶
Python | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Como se muestra en el ejemplo, esto puede causar tiempos de espera no deseados. Por esta razón, se recomienda optar por una arquitectura sin bloqueos (No Lock Contention
) para mejorar la eficiencia del sistema.
Conclusión¶
Es crucial entender los tres tipos de inyección de dependencias (Singleton
, Transient
y Scoped
) y los dos métodos para implementar la inyección de dependencias, ya sea a través de interfaces
o clases
. Esta comprensión te permitirá diseñar sistemas más flexibles, mantenibles y escalables, adaptándose a las necesidades específicas de cada proyecto.