Skip to main content

Inicia sesión en CleanKata

Sigue tu progreso, gana XP y desbloquea todas las lecciones.

Al iniciar sesión aceptas nuestros Términos de uso y Política de privacidad.

Arquitectura Limpia80 XP8 min

Inversión de Dependencias: Control sobre las Dependencias

Con interfaces, los arquitectos hacen que las dependencias del código apunten contra el flujo de control, dando control absoluto sobre el acoplamiento.

Por qué importa

En una pila de llamadas tradicional, el control fluye de los módulos de alto nivel a los de bajo nivel — y la dependencia del código fuente sigue la misma dirección. El módulo de alto nivel importa al de bajo nivel. Eso significa que si cambia la base de datos, también cambian las reglas de negocio. La lógica de negocio está a merced de la infraestructura.

La inversión de dependencias rompe este acoplamiento. Al insertar una interfaz entre la lógica de negocio y la infraestructura, la dependencia del código fuente puede hacerse apuntar contra el flujo de control. El módulo de base de datos depende de la interfaz del repositorio, no al revés. Las reglas de negocio poseen la interfaz; la infraestructura la implementa. Esto da a los arquitectos control absoluto sobre el grafo de acoplamiento — la base de datos, la UI y el framework se convierten en plugins que el módulo principal conecta al inicio, y el núcleo nunca importa ninguno de ellos.

✗El problema

Business logic that directly imports a concrete database driver is coupled to MySQL. Changing the database, mocking it for tests, or running offline is impossible without modifying the business class.

Bad

import mysql.connector  # business logic directly imports DB driver

class MySQLDatabase:
    def __init__(self):
        self.conn = mysql.connector.connect(
            host="localhost", user="root", password="secret"
        )

    def get_user(self, user_id: int) -> dict:
        cur = self.conn.cursor(dictionary=True)
        cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
        return cur.fetchone()

class UserService:
    def __init__(self):
        self.db = MySQLDatabase()  # tightly coupled to MySQL

    def get_profile(self, user_id: int) -> dict:
        return self.db.get_user(user_id)
import { createPool } from "mysql2/promise"; // business code imports infrastructure

class MySQLDatabase {
  private pool = createPool({ host: "localhost", user: "root" });

  async getUser(userId: number): Promise<User> {
    const [rows] = await this.pool.query(
      "SELECT * FROM users WHERE id = ?", [userId]
    );
    return (rows as User[])[0];
  }
}

class UserService {
  private db = new MySQLDatabase(); // coupled: can't swap, can't test without MySQL

  async getProfile(userId: number): Promise<User> {
    return this.db.getUser(userId);
  }
}

✓La solución

The business class depends on a repository abstraction it owns. The MySQL implementation is a plugin wired in from outside — the service never imports it.

Good

from abc import ABC, abstractmethod

class UserRepository(ABC):  # interface owned by business logic
    @abstractmethod
    def get(self, user_id: int) -> dict: ...

class InMemoryUserRepository(UserRepository):  # test plugin
    def __init__(self, data: dict): self._data = data
    def get(self, user_id: int) -> dict: return self._data[user_id]

class UserService:
    def __init__(self, repo: UserRepository):  # depends on abstraction only
        self._repo = repo

    def get_profile(self, user_id: int) -> dict:
        return self._repo.get(user_id)

# Production: pass MySQLUserRepository(); tests: pass InMemory
svc = UserService(InMemoryUserRepository({1: {"name": "Alice"}}))
print(svc.get_profile(1))
interface UserRepository {  // interface owned by business logic
  getUser(userId: number): Promise<User>;
}

class InMemoryUserRepository implements UserRepository {
  constructor(private data: Record<number, User>) {}
  async getUser(id: number): Promise<User> { return this.data[id]; }
}

class UserService {
  constructor(private repo: UserRepository) {} // zero import of MySQL

  async getProfile(userId: number): Promise<User> {
    return this.repo.getUser(userId);
  }
}

// In tests: InMemory. In production: MySQLUserRepository. Service unchanged.
const svc = new UserService(new InMemoryUserRepository({ 1: { name: "Alice" } }));

💡Conclusión clave

Las reglas de negocio nunca deben conocer el nombre de una base de datos, framework o biblioteca de UI — las interfaces permiten a los arquitectos apuntar todas las dependencias del código fuente hacia las políticas que más importan.

🔧 Algunos ejercicios pueden tener errores. Si algo parece incorrecto, usa el botón Feedback (abajo a la derecha) para reportarlo — nos ayuda a corregirlo rápido.

Pista: Las reglas de negocio nunca deberían conocer el nombre de una base de datos, framework o librería de UI.

✗ Tu versión

Inversión de Dependencias: Control sobre las Dependencias — CleanKata — CleanKata