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 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

Desacoplamiento por Capas — CleanKata — CleanKata