Inyección de dependencias con Python



@eduardo_gpg

Número de visitas 2276

Tiempo de lectura 6 min

9 Julio 2021

Sin duda, uno de los mayores retos al que nos enfrentamos como desarrolladores es el crear código legible, fácil de testar, fácil de mantener y sobre todo reutilizable. Afortunadamente para nosotros existen diferentes patrones y técnicas que nos permiten lograr esto. Una de las técnicas más populares es la de Inyección de dependencias, la cual nos permite desarrollar programas con un alto nivel de cohesión y un bajo nivel de acoplamiento. Perfecto para crear programas robustos, escalables y flexibles. 😎

Es por ello que, en esta ocasión, me gustaría explicarte en detalle (Utilizando Python) en qué consiste y cómo podemos implementar la Inyección de dependencias en nuestros desarrollos. 🐍

Es un post muy interesante, en donde hablaremos de buenas practicas de programación, así que te invito a que te quedes.

Bien, sin más introducción, pongamos manos a la obra. 😃

Introducción.

Comencemos con la pregunta obligada ¿Qué la inyección de dependencias? Bien, en términos simples, la inyección de dependencia es un técnica de desarrollo que nos permite escribir código con un alto nivel de cohesión y un bajo nivel de dependencia. Pero ¿Qué significa esto? Bueno, para ello expliquemos los términos de: cohesión y acoplamiento.

Verás, la cohesión no es más que una medida que nos permite conocer qué tanta relación existe para un bloque de código con sigo mismo (Llamase módulos, clases, métodos, funciones etc...). Por ejemplo, si definimos la siguiente clase, podemos decir que su nivel de cohesión es alta, ya que todos los métodos tiene una relación bajo un mismo contexto. Que en este caso sería realizar operaciones para una calculadora. 😎

class Calculadora:

    def suma(numero1, numero2):
        return numero1 + numero2

    def resta(numero1, numero2):
        return numero1 - numero2

    def multiplicacion(numero1, numero2):
        return numero1 * numero2

    def division(numero1, numero2):
        if numero2 == 0:
            raise Exception('No es posible dividir sobre 0')

        return numero1 - numero2

Si por el contrarío, definimos el siguiente módulo, podemos decir que su nivel de cohesión es bajo, ya que las funciones no tienen una relación unas con otras.

# Módulo utils

def obtener_usuario(id):
    pass

def crear_producto(*args):
    pass

def ultimos_envios(max_days):
    pass

def subir_imagen(path):
    pass

Un módulo donde es posible encontrar de todo.

<hr />

Ahora hablemos del acoplamiento, un concepto un poco más sencillo de explicar.

Podemos definir el acoplamiento como la forma en que se relacionan componentes de nuestro código entre ellos. Veamos un ejemplo.

def suma():
    numero_uno = int(input('Ingresa el primer número: '))
    numero_dos = int(input('Ingresa el primer número: '))

    return numero_uno + numero_dos

Para este ejemplo definimos la función suma, función que nos permite sumar 2 números enteros.

Si bien la función cumple con su tarea (sumar dos números enteros), su definición no es la correcta, ya que depende completamente de las función input e int para poder conocer y sumar los 2 números enteros. Esto sin duda limita el uso que podamos darle a dicha función. Por ejemplo, si ahora queremos sumar 2 números enteros que provengan de recursos diferentes (Quizás de una base de datos, un API, un archivo excel etc..) La función queda completamente obsoleta, ya que no esta en su definición el poder trabajar con 2 números enteros que el usuarios no haya ingresado vía teclado. 😦

Lo ideal es que la función se deslinde del cómo obtener los datos, y solo se enfoque en el qué hacer, de esta forma nuestro código será mucho más flexible.

Si realizamos un refactor nuestro código pudiera quedar de la siguiente manera.

def suma(n1, n2):
    return n1 + n2

numero_uno = int(input('Ingresa el primer número: '))
numero_dos = int(input('Ingresa el primer número: '))

resultado = suma(numero_uno, numero_dos)

Mucho mejor, ahora la función tiene una sola tarea, sumar 2 números enteros. La función desconoce completamente de dónde provienen los datos y de lo único que se debe preocupar es de realizar y retornar el resultado de la operación.

Con este refactor no solo reducimos el acoplamiento, si no que aplicamos el principio de responsabilidad única:

  • Una clase debería tener solo una razón para cambiar
  • Si una clase asume más de una responsabilidad, será más sensible al cambio.
  • Si una clase asume más de una responsabilidad, las responsabilidades se acoplan.

Lo ideal es que nuestro código tenga un alto nivel de cohesión y un bajo nivel de acoplamiento.

Inyección de dependencias

Listo, los conceptos fundamentales ya los tenemos claros, así que ahora pasamos al tema principal: Inyección de dependencias.

Lo primero que debemos tener muy en claro es el concepto de dependencia. En términos simples una dependencia es todo lo que una clase necesite para poder funcionar.

Veamos un ejemplo.

class Monitor:
    pass

class Computadora:
    def __init__(self):
       self.monitor= Monitor() # <-- dependency

Para este ejemplo la clase Computadora necesita de la clase Monitor para poder crear una nueva instancia de ella, es decir la clase Computadora depende del la clase Monitor, y su vez la clase Monitor es una dependencia de la clase Computadora. Un objeto de tipo Computadora no puede ser creado sin antes haberse creado un objeto de tipo Monitor. 😲

Al depender la clase Computadora de la clase Monitor, podemos concluir que existe un acoplamiento fuerte, que, cómo hemos visto anteriormente, no es del todo una muy buena idea. Por ejemplo, ¿Qué pasa si queremos modificar las características el monitor? bueno, no podríamos hacerlo, ya que el código es Hard-cod y tendríamos que re definir el método init para ello. 😰

Lo que podemos hacer para solucionar este problema es simplemente dejar de crear la instancia de Monitor dentro del la clase Computadora. Para ello podemos implementar el patrón de Inyección de dependencias.

Tenemos, principalmente, 3 tipos de inyección de dependencias.

  • Constructor injection.
  • Property injection.
  • Method injection.

Para este post implementaremos el tipo Constructor injection. Para ello pasaremos mediante argumentos, al momento de crear un nuevo objeto, todas las dependencias necesarias para nuestra clase. Esto nos permitirá crear objetos con dependencias variadas según necesidades. 🥳

class Monitor:
    pass

class Computadora:
    def __init__(self, monitor: Monitor):
       self.monitor= monitor

Mucho mejor. Ahora es posible pasar al constructor cualquier objeto de tipo Monitor. Inclusive, si somos un poco más abstractos, podemos pasar como argumento sub tipos de la clase Monitor. 🤯

class Monitor:
    pass

class MonitorBenQ(Monitor):
    pass

class MonitorHP(Monitor):
    pass

class Computadora:
    def __init__(self, monitor: Monitor):
       self.monitor= monitor}

monitor_hp = HP()
computadora = Computadora(monitor_hp )

¿Cool no lo crees? Bajo esta premisa pudiéramos crear cualquier tipo de computador con los componentes y periféricos que necesitemos.

Una muy buena analogía para comprender el tema de inyección de dependencias es ver a las dependencias cómo si de piezas de lego se tratasen. Pudiendo reemplazarla unas por otras dependiendo de nuestras necesidades.

Ahora trabajemos con otro ejemplo para que nos quede mucho más en claro, un ejemplo un poco más realista.

Para ello he definido 2 nuevas clases: Config y DataBaseConnect.

class Config:
    def __init__(self, database, user, password, host, port):
        self.database = database
        self.username = username
        self.password = password

        self.host= host
        self.port = port


class DataBaseConnect:
    def __init__(self, config: Config):
       self.config= Config

Utilizando Constructor injection es posible pasar a la conexión a de la base de datos diferentes configuraciones según necesidades. Quizás una configuración para desarrollo, producción o testing.

development = Config('pywombat', 'root', 'password', 'localhost', 2207)

production = Config('pywombat', 'superadmin', 'password', '157.245.120.121', 2207)

testing= Config('pywombat', 'test', 'password', '157.245.120.121', 2207)

Dependiendo del entornos que nos encontremos utilizamos la configuración necesaria y listo.

connect = DataBaseConnect(development)
connect = DataBaseConnect(production)
connect = DataBaseConnect(testing)

Esto a sus vez, por supuesto, nos permite testear nuestro código de una forma mucho más sencilla, para diferentes escenarios. 🐙

def test_development_connection:
    development = Config('pywombat', 'root', 'password', 'localhost', 2207)
    connect = DataBaseConnect(development)

    assert connect = True, 'No es posible realizar la conexión'

def test_production_connection:
    production = Config('pywombat', 'superadmin', 'password', '157.245.120.121', 2207)
    connect = DataBaseConnect(production)

    assert connect = True, 'No es posible realizar la conexión'

def test_test_connection:
    testing= Config('pywombat', 'test', 'password', '157.245.120.121', 2207)
    connect = DataBaseConnect(testing)

    assert connect = True, 'No es posible realizar la conexión'

Conclusión

En conclusión, la inyección de dependencia es sin duda uno de los patrones de desarrollo que sin duda debemos tener en consideración al momento de desarrollar nuestros proyectos; pudiendo desacoplar los componentes de nuestra aplicación de una forma muy sencilla, delegando responsabilidades únicas a diferentes piezas de software, lo cual a su vez nos permitirá tener código mucho más flexible, fácil de testear, fácil de leer y sobre todo, fácil de mantener. 🍻


¿El contenido te resulto de ayuda?

Para poder dejar tu opinión es necesario ser un usuario autenticado. Login

Más Tips y Ejercicios 🐍

Adquiere una subscripción PyWombat por tan solo $3 USD. al mes.

Conoce los beneficios de ser usuario premium:
Niveles desbloqueados: Ten accesos a todos los niveles de ejercicios. 🔓
Nuevo límite: Incrementa tu límite de ejercicios por semana. 🚀
Contenido único: Recibe semanalmente recursos exclusivos de Python (Videos, Artículos y Capitulos del libro PyWombat, comienza como desarrollador Python. 🐍