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

Límites Parciales: Estrategias de Anticipación

Los límites arquitectónicos completos son costosos — los límites parciales usando patrones Strategy o Facade preservan puntos de separación futura sin el costo total inicial.

Por qué importa

Una frontera arquitectónica completa — con interfaces bidireccionales, componentes separados y despliegue independiente — es costosa de construir y mantener. En las primeras etapas de un proyecto, puedes anticipar que se necesitará una frontera pero no puedes justificar el costo todavía. Las fronteras parciales son la respuesta: construyes la mayoría de las piezas, pero te detienes en el paso final de separación.

Robert Martin describe tres estrategias de fronteras parciales. Omitir el último paso: construye todas las clases e interfaces necesarias para una frontera completa, pero mantenlas en un solo componente. Frontera unidimensional: usa el patrón Strategy para aislar una dirección de cambio — la ubicación de la frontera está preparada, pero solo un lado tiene una interfaz. Facade: coloca una interfaz pública más simple frente a un subsistema complejo para ocultarlo de los consumidores.

La inversión: estas estrategias cuestan menos que una frontera completa hoy, y mucho menos que una refactorización dolorosa mañana. El equipo futuro que necesite extraer la frontera a un servicio separado encontrará las costuras ya preparadas.

✗El problema

Notification logic hardcoded with if/elif — every new channel requires editing this class, risking regressions in existing channels.

Bad

class NotificationService:
    def send(self, user_id: str, message: str, notification_type: str) -> None:
        if notification_type == "email":
            print(f"[EMAIL] to user {user_id}: {message}")
        elif notification_type == "sms":
            print(f"[SMS] to user {user_id}: {message}")
        elif notification_type == "push":
            print(f"[PUSH] to user {user_id}: {message}")
        else:
            raise ValueError(f"Unknown type: {notification_type}")

# Each new channel: open this class, add an elif, risk breaking the others.
export class NotificationService {
  send(userId: string, message: string, type: "email" | "sms" | "push"): void {
    if (type === "email") {
      console.log(\`[EMAIL] to \${userId}: \${message}\`);
    } else if (type === "sms") {
      console.log(\`[SMS] to \${userId}: \${message}\`);
    } else if (type === "push") {
      console.log(\`[PUSH] to \${userId}: \${message}\`);
    } else {
      throw new Error(\`Unknown type: \${type}\`);
    }
  }
}
// Every new channel: open this class, add an else-if, risk breaking the others.

✓La solución

A Strategy-based partial boundary — new notification channels are added as new classes; no existing code is ever modified. The boundary seam is prepared for future extraction.

Good

from abc import ABC, abstractmethod

class NotificationStrategy(ABC):
    @abstractmethod
    def send(self, user_id: str, message: str) -> None: ...

class EmailNotification(NotificationStrategy):
    def send(self, user_id: str, message: str) -> None:
        print(f"[EMAIL] to user {user_id}: {message}")

class SMSNotification(NotificationStrategy):
    def send(self, user_id: str, message: str) -> None:
        print(f"[SMS] to user {user_id}: {message}")

# Adding push: new class — zero changes to Email or SMS:
class PushNotification(NotificationStrategy):
    def send(self, user_id: str, message: str) -> None:
        print(f"[PUSH] to user {user_id}: {message}")

class NotificationService:
    def __init__(self, strategy: NotificationStrategy):
        self._strategy = strategy

    def send(self, user_id: str, message: str) -> None:
        self._strategy.send(user_id, message)

# Still one deployable unit (partial boundary).
# If needed later, each strategy can be extracted to its own service.
export interface NotificationStrategy {
  send(userId: string, message: string): void;
}

export class EmailNotification implements NotificationStrategy {
  send(userId: string, message: string): void {
    console.log(\`[EMAIL] to \${userId}: \${message}\`);
  }
}

export class SMSNotification implements NotificationStrategy {
  send(userId: string, message: string): void {
    console.log(\`[SMS] to \${userId}: \${message}\`);
  }
}

// Adding push: new class — zero changes to existing code:
export class PushNotification implements NotificationStrategy {
  send(userId: string, message: string): void {
    console.log(\`[PUSH] to \${userId}: \${message}\`);
  }
}

export class NotificationService {
  constructor(private readonly strategy: NotificationStrategy) {}

  send(userId: string, message: string): void {
    this.strategy.send(userId, message);
  }
}
// Partial boundary: one component today, ready to split into services tomorrow.

💡Conclusión clave

Una frontera parcial es una inversión en el futuro — cuesta menos que una frontera completa hoy y mucho menos que una refactorización mañana. Usa Strategy o Facade para marcar dónde eventualmente se necesitará una frontera, sin pagar el precio completo de un componente desplegable separado hasta que el proyecto genuinamente lo requiera.

🔧 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 límite parcial es una inversión en el futuro — cuesta menos que un límite completo hoy y mucho menos que una refactorización mañana.

✗ Tu versión