Desacoplamiento por Capas
Separa lo que cambia a diferentes ritmos — UI, reglas de negocio de aplicación, reglas de dominio y base de datos cambian por razones distintas y deben ser capas horizontales.
Por qué importa
Diferentes partes de un sistema cambian por razones completamente distintas. La UI cambia cuando el equipo de diseño tiene nuevas ideas o un cliente solicita un nuevo formato de vista. Las reglas de negocio de aplicación (lógica de casos de uso) cambian cuando se requieren nuevos flujos de trabajo. Las reglas de negocio de dominio (lógica de entidades) cambian cuando cambian las políticas fundamentales del negocio — el tipo más raro. La base de datos cambia cuando se adopta una nueva tecnología o se optimiza el esquema. Estos son cuatro ejes de cambio distintos.
Las capas horizontales aplican el Principio de Responsabilidad Única a escala arquitectónica: cada capa tiene exactamente una razón para cambiar. Las dependencias apuntan hacia adentro — la capa de UI conoce la capa de aplicación, que conoce la capa de dominio, que no conoce a nadie. Un cambio en la UI nunca puede romper una regla de dominio. Una migración de esquema nunca puede requerir tocar la lógica de negocio.
✗El problema
A single views.py mixing all four layers — HTTP parsing, use-case orchestration, discount logic, persistence, and email — changing the UI requires touching the database and the domain rule.
Bad
from flask import request, jsonify
from db import session
from models import Order, User, Item
import smtplib
def place_order():
data = request.get_json()
user_id, item_id, qty = data["user_id"], data["item_id"], data["qty"]
item = session.query(Item).get(item_id)
if item.stock < qty:
return jsonify({"error": "Out of stock"}), 400
user = session.query(User).get(user_id)
price = item.price * qty
if user.is_premium:
price *= 0.9 # domain rule buried inside HTTP handler
order = Order(user_id=user_id, item_id=item_id, qty=qty, total=price)
session.add(order)
session.commit()
smtp = smtplib.SMTP("localhost")
smtp.sendmail("shop@co.com", user.email, f"Total: {price}")
return jsonify({"order_id": order.id, "total": price})
import { Request, Response } from "express";
import { AppDataSource } from "./data-source";
import { sendEmail } from "./mailer";
export async function placeOrder(req: Request, res: Response) {
const { userId, itemId, qty } = req.body;
const item = await AppDataSource.getRepository(ItemEntity)
.findOne({ where: { id: itemId } });
if (!item || item.stock < qty) { res.status(400).json({ error: "Out of stock" }); return; }
const user = await AppDataSource.getRepository(UserEntity)
.findOne({ where: { id: userId } });
let total = item.price * qty;
if (user?.isPremium) total *= 0.9; // domain rule here
const order = AppDataSource.getRepository(OrderEntity)
.create({ userId, itemId, qty, total });
await AppDataSource.getRepository(OrderEntity).save(order);
await sendEmail(user!.email, `Order total: ${total}`);
res.json({ orderId: order.id, total });
}
✓La solución
Four layers — each ignorant of the one above it. The UI calls the use case, the use case calls the domain entity, the entity knows nothing about anyone.
Good
# domain/order.py — innermost, most stable (no imports from above)
from decimal import Decimal
class Order:
def __init__(self, item_price: Decimal, qty: int, is_premium: bool):
subtotal = item_price * qty
self.total = subtotal * (Decimal("0.9") if is_premium else Decimal("1"))
# application/place_order.py — use-case orchestration
class PlaceOrderUseCase:
def __init__(self, item_repo, order_repo, notifier):
self._items, self._orders, self._notifier = item_repo, order_repo, notifier
def execute(self, user_id, item_id, qty, is_premium) -> dict:
item = self._items.get(item_id)
if item.stock < qty:
raise ValueError("Out of stock")
order = Order(item.price, qty, is_premium)
saved = self._orders.save(order, user_id)
self._notifier.send(user_id, saved.total)
return {"order_id": saved.id, "total": str(saved.total)}
# api/routes.py — HTTP boundary (outermost, changes for UI reasons only)
# @app.post("/orders")
# def place_order():
# data = request.get_json()
# result = PlaceOrderUseCase(sql_items, sql_orders, email_notifier).execute(**data)
# return jsonify(result)
// domain/Order.ts — innermost (no imports from any layer above)
export class Order {
readonly total: number;
constructor(itemPrice: number, qty: number, isPremium: boolean) {
const subtotal = itemPrice * qty;
this.total = isPremium ? subtotal * 0.9 : subtotal;
}
}
// application/PlaceOrderUseCase.ts — orchestrates domain + repositories
export class PlaceOrderUseCase {
constructor(
private items: ItemRepository,
private orders: OrderRepository,
private notifier: Notifier,
) {}
async execute(userId: string, itemId: string, qty: number, isPremium: boolean) {
const item = await this.items.get(itemId);
if (!item || item.stock < qty) throw new Error("Out of stock");
const order = new Order(item.price, qty, isPremium);
const saved = await this.orders.save(order, userId);
await this.notifier.send(userId, saved.total);
return { orderId: saved.id, total: saved.total };
}
}
// api/orderRoutes.ts — UI layer (knows about HTTP, knows about use case)
// router.post("/orders", async (req, res) => {
// const result = await new PlaceOrderUseCase(pgItems, pgOrders, emailNotifier)
// .execute(req.body.userId, req.body.itemId, req.body.qty, req.user.isPremium);
// res.json(result);
// });
💡Conclusión clave
Cuando un cambio en la UI requiere tocar la capa de base de datos, tus capas no son realmente capas — son una gran bola de barro. Las verdaderas capas cambian independientemente: un nuevo formato de respuesta de API nunca toca un cálculo de descuento, y una migración de esquema nunca toca una ruta HTTP.
🔧 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: Cuando un cambio de UI requiere tocar la capa de base de datos, tus capas no son realmente capas — son una gran bola de barro.
✗ Tu versión