Dependency Inversion Principle en GO.

2024-11-18

arquitectura

Comencemos definiendo qué establece el Dependency Injection Principle (DIP). Si consultamos alguna definición en la literatura, podríamos traducirla del inglés como: "El código que implementa políticas de alto nivel no debe depender de detalles de bajo nivel". Esta es la explicación que nos ofrece Robert C. Martin en Clean Architecture, un libro que, en mi opinión, sigue teniendo conceptos muy interesantes.

Bien, ya tenemos una definición, pero ¿qué significa en la práctica? Básicamente, el DIP nos indica que la lógica de negocio de tu aplicación o sistema no debe depender de detalles de bajo nivel. Por ejemplo, no debería importar qué mecanismo de persistencia utilizas, qué protocolo de comunicación empleas (HTTP, FTP, sockets u otros), ni tampoco debería estar acoplada a los frameworks que uses.

Entonces, ¿cómo lo implementamos?

Veamos un ejemplo: imagina una aplicación de consola diseñada para crear una lista de tareas. Desde la terminal, con unos pocos comandos, puedes agregar tareas a tu lista, eliminarlas o marcarlas como "en progreso" o "terminadas". Las tareas se guardan en un archivo JSON ubicado en el directorio donde ejecutaste el comando. La aplicación se llama task-cli. Abajo un ejemplo de cómo añadir una tarea a tu lista.

$ task-cli add "Escribir un árticulo sobre DIP" #crea un archivo "tasks.json" en el directorio actual.

Para entender mejor la inversión de dependencia, primero voy a definir cuáles son las políticas de alto nivel de task-cli. Básicamente, son: manejar una lista de tareas, agregar, eliminar, cambiar estados y actualizar tareas. Básico pero letal. Cualquier otra funcionalidad que quede fuera de estas cuatro se considera un detalle de bajo nivel.

Entonces, ¿cuál es el detalle de bajo nivel para task-cli? La creación del archivo JSON.

Aquí tienes el diagrama de "clases" (entre comillas porque task-cli está escrita en Go, y en Go no existen las clases... creo).

task-cli diagrama de clases

Ahora, detallemos el diagrama. Lo primero es la división en tres componentes: UI, Domain y DB.

  1. UI: Este componente se encarga de la interacción con la terminal y actúa como intermediario entre los demás componentes, manejando la inyección de dependencias.
  2. Domain: Es el núcleo de task-cli, lógica de negocio, donde residen las políticas de alto nivel.
  3. DB: La persistencia, representa el detalle de bajo nivel.

Pasemos a las "clases". Primero, los comandos. La clase Command engloba las distintas acciones disponibles. Como ejemplo, tenemos AddCommand, que, como su nombre lo indica, es el comando para agregar tareas.

Luego, en un nivel más bajo, encontramos la clase Task, que simplemente representa una tarea como objeto. Sin embargo, lo más importante es la clase Tasks (en plural). Este es el objeto de dominio, encargado de contener TODA (por ahora...) la lógica de negocio de task-cli.

Finalmente, llegamos al detalle de bajo nivel: la implementación de la interfaz TaskRepository por parte de FileDataBase. En esta parte del diagrama podemos ver el DIP en acción, Domain, política de alto nivel, NO depende de DB, el detalle de bajo nivel sino al contrario.

Consideraciones clave para una buena implementación del DIP:

  1. La interfaz (TaskRepository) debe estar definida dentro de tu dominio, y
  2. La inversión de dependecia se ve reflejada cuando el flujo de datos va en dirección contraria a la línea de dependencia.

¿Estamos de acuerdo en que el segundo punto puede resultar un poco confuso? No te preocupes, una imagen vale más que mil palabras.

Dependency inversion diagrama de classes

Si ejecuto el AddCommand en una terminal:

$ task-cli add "Escribir un árticulo sobre DIP"

Los datos viajan en el sentido de la flecha roja, la tarea que agregamos viaja desde Domain hacia DB. Sin embargo, la dependencia es desde DB hacia Domain.

¿Y qué pasa con el primer punto?

El punto uno es igual de crucial. Si defines la interfaz fuera de tu dominio, no habría tal inversión de dependencia. ¿Por qué? Porque la clase Tasks y el componente Domain (política de alto nivel) dependerían de una interfaz definida en DB, que es el detalle de bajo nivel.

¿Y qué sucede entonces? Pues que los datos viajarían en la misma dirección que la dependencia, rompiendo la idea del DIP. ¡Un problema, no crees? Mira la imagen abajo.

como no hace dependecy inversion

Imagina que el componente DB viene de un tercero, y ese tercero cambia la interfaz en la última versión, task-cli dejaría de funcionar al instante que actualicemos el componente DB.

Espero que lo haya podido explicar de forma clara. Al menos escribir este post ayudó a que el concepto se quedara en mi cabeza. Por cierto, aquí les dejo el repo de task-cli.