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 Limpia70 XP7 min

Desacoplamiento de Casos de Uso

Los casos de uso son cortes verticales que atraviesan todas las capas — desacoplarlos permite que cada uno evolucione independientemente sin que los cambios de un caso de uso colisionen con los de otro.

Por qué importa

Las capas horizontales (UI, casos de uso, dominio, base de datos) separan las preocupaciones por ritmo de cambio. Pero los casos de uso proporcionan una segunda dimensión: cortes verticales que separan las preocupaciones por intención. Agregar un pedido es completamente diferente a eliminarlo. Aprobar un pedido involucra diferentes reglas, diferentes actores y diferente riesgo que enviarlo. Cada uno es un escenario de negocio discreto con su propio camino de entrada a salida.

Cuando todos estos escenarios viven como métodos en una única clase controlador, agregar un campo a AddOrder arriesga romper las pruebas de DeleteOrder. Dos equipos trabajando en diferentes casos de uso siguen produciendo conflictos de fusión en el mismo archivo. Los casos de uso desacoplados — cada uno una clase en su propio archivo — significa que cada equipo posee su tira vertical independientemente. Los cambios están aislados. Las pruebas están enfocadas. Los equipos no colisionan.

✗El problema

A monolithic OrderController with five use cases as methods — all scenarios coupled together, changing one risks breaking all others.

Bad

class OrderController:
    def __init__(self, db, mailer, payments):
        self.db, self.mailer, self.payments = db, mailer, payments

    def add(self, user_id, item_id, qty):
        # 40 lines: stock check, total, payment, notification...
        order = {"user_id": user_id, "item_id": item_id, "qty": qty}
        self.db.insert("orders", order)
        self.payments.charge(user_id, self._total(item_id, qty))
        self.mailer.send(user_id, "Order confirmed")

    def delete(self, order_id):
        order = self.db.find("orders", order_id)
        if order["status"] != "pending":
            raise ValueError("Cannot delete")
        self.db.delete("orders", order_id)

    def approve(self, order_id, manager_id): ...
    def cancel(self, order_id, reason): ...
    def ship(self, order_id, tracking): ...

# Team A edits add(). Team B edits approve(). Merge conflict guaranteed.
# A bug in approve() fails tests for delete() and add().
export class OrderController {
  constructor(
    private db: Database,
    private mailer: Mailer,
    private payments: PaymentGateway,
  ) {}

  async add(userId: string, itemId: string, qty: number) {
    const total = await this.calculateTotal(itemId, qty);
    const id    = await this.db.insert("orders", { userId, itemId, qty, total });
    await this.payments.charge(userId, total);
    await this.mailer.send(userId, `Order ${id} confirmed`);
    return id;
  }
  async delete(orderId: string) { /* ... */ }
  async approve(orderId: string, managerId: string) { /* ... */ }
  async cancel(orderId: string, reason: string) { /* ... */ }
  async ship(orderId: string, tracking: string) { /* ... */ }
}
// 5 use cases. 1 file. Entire class must be understood to change anything.

✓La solución

Each use case is its own class — independently testable, independently owned, independently changeable.

Good

# application/add_order.py
class AddOrderUseCase:
    def __init__(self, order_repo, stock_service, payment_gateway, notifier):
        self._orders   = order_repo
        self._stock    = stock_service
        self._payments = payment_gateway
        self._notifier = notifier

    def execute(self, user_id: str, item_id: str, qty: int) -> str:
        self._stock.reserve(item_id, qty)
        total    = self._stock.price(item_id) * qty
        order_id = self._orders.create(user_id, item_id, qty, total)
        self._payments.charge(user_id, total)
        self._notifier.send(user_id, order_id)
        return order_id

# application/delete_order.py
class DeleteOrderUseCase:
    def __init__(self, order_repo):
        self._orders = order_repo

    def execute(self, order_id: str) -> None:
        order = self._orders.get(order_id)
        if order.status != "pending":
            raise ValueError("Only pending orders can be deleted")
        self._orders.delete(order_id)

# application/approve_order.py — independent of the above, testable alone.
// application/AddOrderUseCase.ts
export class AddOrderUseCase {
  constructor(
    private orders:   OrderRepository,
    private stock:    StockService,
    private payments: PaymentGateway,
    private notifier: Notifier,
  ) {}

  async execute(userId: string, itemId: string, qty: number): Promise {
    await this.stock.reserve(itemId, qty);
    const price   = await this.stock.getPrice(itemId);
    const total   = price * qty;
    const orderId = await this.orders.create(userId, itemId, qty, total);
    await this.payments.charge(userId, total);
    await this.notifier.send(userId, orderId);
    return orderId;
  }
}

// application/DeleteOrderUseCase.ts
export class DeleteOrderUseCase {
  constructor(private orders: OrderRepository) {}

  async execute(orderId: string): Promise {
    const order = await this.orders.get(orderId);
    if (order.status !== "pending") throw new Error("Only pending orders can be deleted");
    await this.orders.delete(orderId);
  }
}
// Team A works on AddOrderUseCase. Team B works on DeleteOrderUseCase.
// No merge conflicts. No shared mutable state. Each test file is minimal.

💡Conclusión clave

Un caso de uso debe ser un sustantivo, no un método en un objeto dios. Cada caso de uso posee su propio camino de entrada a salida — puede cambiarse, probarse, poseerse y desplegarse sin tocar ningún otro caso de uso.

🔧 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: Un caso de uso debe ser un sustantivo, no un método en un objeto dios. Cada caso de uso posee su propio camino de entrada a salida.

✗ Tu versión

Desacoplamiento de Casos de Uso — CleanKata — CleanKata