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

Mantener las Opciones Abiertas

Un buen arquitecto maximiza el número de decisiones aún no tomadas — separando la política de los detalles para que las elecciones de base de datos y framework puedan postponerse hasta saber más.

Por qué importa

Todo sistema de software puede dividirse en dos categorías: política y detalles. La política contiene las reglas y procedimientos de negocio — el verdadero valor del sistema. Los detalles son las cosas que permiten a personas, máquinas y otros sistemas comunicarse con la política: bases de datos, frameworks web, dispositivos de E/S. Los detalles son irrelevantes para la política. Nunca deberían influir en ella.

Un buen arquitecto reconoce esto y maximiza el número de decisiones aún no tomadas. Cuanto más retrases comprometerte con una tecnología de base de datos, más sabrás sobre patrones de tráfico, formas de consulta y necesidades de consistencia. Un sistema cuya lógica de negocio funciona con un repositorio en memoria demuestra que la base de datos es un detalle — puedes conectar la real cuando la elección esté bien informada.

✗El problema

Business logic class that directly imports the ORM, queries a DB model, and formats HTML — policy and details welded together, impossible to test without a real database.

Bad

from sqlalchemy.orm import Session
from models import User, Subscription

class UserService:
    def __init__(self, session: Session):
        self.session = session

    def get_welcome_page(self, user_id: int) -> str:
        user = self.session.query(User).filter(User.id == user_id).first()
        if not user:
            return "<h1>Not found</h1>"
        sub = self.session.query(Subscription).filter(
            Subscription.user_id == user_id, Subscription.active == True
        ).first()
        tier = "Premium" if sub else "Free"
        return f"<h1>Welcome {user.name} ({tier})</h1>"

# Test requires a real database with the full ORM schema.
# Switching to MongoDB: rewrite UserService entirely.
# The database is not a detail — it is a core dependency.
import { DataSource } from "typeorm";
import { UserEntity } from "./entities/User";
import { SubscriptionEntity } from "./entities/Subscription";

export class UserService {
  constructor(private ds: DataSource) {}

  async getWelcomePage(userId: number): Promise {
    const user = await this.ds.getRepository(UserEntity)
      .findOne({ where: { id: userId } });
    if (!user) return "<h1>Not found</h1>";
    const sub = await this.ds.getRepository(SubscriptionEntity)
      .findOne({ where: { userId, active: true } });
    const tier = sub ? "Premium" : "Free";
    return `<h1>Welcome ${user.name} (${tier})</h1>`;
  }
}
// Test requires TypeORM DataSource with migrations.
// The option to swap the database has already been closed.

✓La solución

UserService depends only on an abstract repository — the database is a plugin you choose when you're ready.

Good

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class UserProfile:
    name: str
    tier: str  # "free" | "premium"

class UserRepository(ABC):
    @abstractmethod
    def get_profile(self, user_id: int) -> UserProfile | None: ...

class UserService:
    def __init__(self, repo: UserRepository):
        self._repo = repo

    def get_welcome_message(self, user_id: int) -> str:
        profile = self._repo.get_profile(user_id)
        if not profile:
            return "User not found"
        label = "Premium" if profile.tier == "premium" else "Free"
        return f"Welcome {profile.name} ({label})"

# Test with zero infrastructure:
class InMemoryUserRepo(UserRepository):
    def __init__(self, data): self._data = data
    def get_profile(self, uid): return self._data.get(uid)

# The database option is still open. Commit when you know enough.
export interface UserProfile { name: string; tier: "free" | "premium"; }

export interface UserRepository {
  getProfile(userId: number): Promise;
}

export class UserService {
  constructor(private repo: UserRepository) {}

  async getWelcomeMessage(userId: number): Promise {
    const profile = await this.repo.getProfile(userId);
    if (!profile) return "User not found";
    const label = profile.tier === "premium" ? "Premium" : "Free";
    return `Welcome ${profile.name} (${label})`;
  }
}

// In tests — no DB, no ORM, no network:
const stub: UserRepository = {
  getProfile: async (id) => id === 1 ? { name: "Alice", tier: "premium" } : null,
};
// In production — TypeORM or Prisma adapter implements UserRepository.
// Decision deferred. Option kept open.

💡Conclusión clave

Si tu lógica de negocio no puede ejecutarse sin una conexión a base de datos, la base de datos no es un detalle — es una dependencia central que nunca cuestionaste. Separa la política de los detalles, y retrasa comprometerte con los detalles hasta que tengas suficiente información para elegir sabiamente.

🔧 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: Si tu lógica de negocio no puede ejecutarse sin una conexión a la base de datos, la base de datos no es un detalle — es una dependencia central que nunca cuestionaste.

✗ Tu versión