Inyecci贸n De Dependencias Con Python

Fecha de publicaci贸n: 9 Julio 2021
Tiempo de lectura: 6 min.
Premium: False
N煤mero de visitas: 731


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.


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. 馃嵒

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. 馃悕