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