El Diablo Está en los Detalles de Implementación
La mejor arquitectura fracasa sin un uso disciplinado de los modificadores de acceso — el compilador es tu aliado más fuerte para hacer cumplir las reglas de Clean Architecture en todo el equipo.
Por qué importa
Simon Brown identifica cuatro niveles en los que puede implementarse la arquitectura. Entender en qué nivel está tu sistema — y en qué nivel necesita estar — es un juicio arquitectónico clave:
- 1.Arquitectura como idea — diagramas en un wiki, cajas y flechas. Ningún código refleja el diagrama. Cualquier desarrollador puede violar el diagrama en cualquier momento. Aplicación cero.
- 2.Arquitectura en paquetes — la estructura de directorios refleja el diagrama. Pero todas las clases son públicas. Cualquier clase puede importar a cualquier otra. La aplicación es completamente social — "por favor no te saltes el servicio."
- 3.Arquitectura aplicada por modificadores de acceso — internos de paquete privados, solo se expone la API pública. El compilador hace las violaciones imposibles. Este es el mínimo para una frontera arquitectónica real.
- 4.Arquitectura aplicada por unidades de despliegue — JARs, paquetes o servicios separados. El runtime hace las violaciones imposibles. Se usa cuando los equipos o la escala lo requieren.
La mayoría de las bases de código están en el nivel 2 y creen que están en el nivel 3. La diferencia entre los niveles 2 y 3 es la diferencia entre arquitectura aspiracional y arquitectura real. El tamaño del equipo y la disciplina determinan qué nivel es apropiado — pero ningún equipo tiene la disciplina suficiente para resistir permanentemente la conveniencia de una importación directa.
✗El problema
Perfect architectural diagram. In practice, every class is public. A developer imports OrderRepository directly into a background task — unnoticed, no compiler complaint.
✗ Bad — Level 2: Architecture in packages only
# orders/order_repository.py ← should be internal, but is public
class OrderRepository:
def find_by_id(self, order_id: str) -> dict:
return {"id": order_id, "total": 99.0, "status": "pending"}
def mark_shipped(self, order_id: str) -> None:
pass
# orders/order_service.py ← intended public API
class OrderService:
def get_order(self, order_id: str) -> dict:
return OrderRepository().find_by_id(order_id)
# tasks/shipping_task.py ← Celery worker — bypasses OrderService!
from orders.order_repository import OrderRepository # ← no compiler warning
from celery import Celery
app = Celery("tasks")
@app.task
def process_shipment(order_id: str):
repo = OrderRepository()
order = repo.find_by_id(order_id) # bypasses all business validation
repo.mark_shipped(order_id) # side effects without domain rules
# Architecture diagram: correct. Code: level 2. Violation: undetected.
// orders/OrderRepository.ts ← should be internal, but exported
export class OrderRepository {
findById(orderId: string): { id: string; total: number; status: string } {
return { id: orderId, total: 99.0, status: "pending" };
}
markShipped(orderId: string): void { /* database write */ }
}
// orders/OrderService.ts ← intended public API
export class OrderService {
getOrder(orderId: string) { return new OrderRepository().findById(orderId); }
}
// workers/ShippingWorker.ts ← background job — bypasses OrderService!
import { OrderRepository } from "../orders/OrderRepository"; // ← no compiler warning
export class ShippingWorker {
process(orderId: string): void {
const repo = new OrderRepository();
repo.markShipped(orderId); // no business validation, no domain rules
}
}
// Architecture diagram: correct. Code: level 2. Violation: undetected.
✓La solución
__all__ (Python) or barrel index.ts (TypeScript) makes OrderRepository invisible to the rest of the system. The architecture is code, not a diagram.
✓ Good — Level 3: Architecture enforced by access modifiers
# orders/__init__.py ← enforced public API
from orders._order_service import OrderService
__all__ = ["OrderService"] # only OrderService is the public contract
# orders/_order_repository.py ← private (underscore + not in __all__)
class _OrderRepository:
def find_by_id(self, order_id: str) -> dict:
return {"id": order_id, "total": 99.0, "status": "pending"}
def mark_shipped(self, order_id: str) -> None:
pass
# orders/_order_service.py ← private implementation, exported via __init__
from orders._order_repository import _OrderRepository
class OrderService:
def ship_order(self, order_id: str) -> None:
order = _OrderRepository().find_by_id(order_id)
if order["status"] != "pending":
raise ValueError("Cannot ship an order that is not pending")
_OrderRepository().mark_shipped(order_id)
# tasks/shipping_task.py ← must use the public API
from orders import OrderService # _OrderRepository is not in __all__
from celery import Celery
app = Celery("tasks")
@app.task
def process_shipment(order_id: str):
OrderService().ship_order(order_id) # goes through business validation
# Architecture is enforced by __all__ + linting rules (flake8-import-order).
# A violation raises a linting error at development time, not production.
// orders/OrderRepository.ts ← NOT exported from barrel (internal only)
class OrderRepository {
findById(orderId: string): { id: string; total: number; status: string } {
return { id: orderId, total: 99.0, status: "pending" };
}
markShipped(orderId: string): void { /* database write */ }
}
export { OrderRepository }; // visible inside orders/ module only
// orders/OrderService.ts
import { OrderRepository } from "./OrderRepository";
export class OrderService {
shipOrder(orderId: string): void {
const order = new OrderRepository().findById(orderId);
if (order.status !== "pending") {
throw new Error("Cannot ship an order that is not pending");
}
new OrderRepository().markShipped(orderId);
}
}
// orders/index.ts ← barrel: only public surface
export { OrderService } from "./OrderService";
// OrderRepository is NOT re-exported — invisible to the rest of the system
// workers/ShippingWorker.ts ← must use the barrel
import { OrderService } from "../orders"; // barrel import only
export class ShippingWorker {
process(orderId: string): void {
new OrderService().shipOrder(orderId); // goes through business validation
}
}
// ESLint: "import/no-internal-modules" rule.
// Attempting to import "../orders/OrderRepository" directly → lint error
// at development time, not production. Architecture is code, not a diagram.
💡Conclusión clave
Una arquitectura que solo puede aplicarse pidiendo a los desarrolladores que tengan cuidado no es una arquitectura — es una sugerencia. Usa el compilador. En Python, usa __all__ y prefijos de guion bajo. En TypeScript, usa archivos barrel index.ts y la regla ESLint import/no-internal-modules. La diferencia entre un diagrama y una arquitectura es la aplicación.
🔧 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: Una arquitectura que solo puede hacerse cumplir pidiendo a los desarrolladores que tengan cuidado no es una arquitectura — es una sugerencia. Usa el compilador.
✗ Tu versión