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