Patrón Business Delegate
Reduce el acoplamiento entre capas de presentación y negocio introduciendo un intermediario que maneja lookup, creación e invocación de servicios de negocio.
Visión general
El Patrón Business Delegate reduce el acoplamiento entre la capa de presentación y la capa de servicios de negocio introduciendo una capa intermediaria. En lugar de que la capa de presentación acceda directamente a servicios de negocio (EJBs, APIs remotas, u objetos de servicio complejos), utiliza un Business Delegate que maneja la búsqueda, creación e invocación del servicio.
Este patrón es particularmente valioso en aplicaciones enterprise donde los servicios de negocio pueden estar distribuidos, cambiar con frecuencia o requerir una inicialización compleja. El Business Delegate también puede guardar resultados en caché, gestionar reintentos y ofrecer una interfaz simplificada a la capa de presentación.
Cuándo Usar
Usa el Patrón Business Delegate cuando:
- La capa de presentación necesita acceder a servicios de negocio remotos o distribuidos
- La búsqueda del servicio de negocio es compleja o involucra JNDI, registros de servicios o contenedores DI
- Quieres guardar en caché las referencias de servicios para evitar búsquedas repetidas
- La API de la capa de negocio es compleja y necesitas una fachada simplificada para presentación
Cuándo Evitar
- Aplicaciones monolíticas simples donde presentación y negocio no están separados
- Cuando una fachada simple, un Context Object o la inyección directa de servicio es suficiente
- El overhead de una capa adicional no está justificado por la reducción de acoplamiento
Solución
Python
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any
class OrderServiceInterface(ABC):
@abstractmethod
def create_order(self, customer_id: str, items: list) -> dict:
pass
@abstractmethod
def get_order(self, order_id: str) -> dict:
pass
class RemoteOrderService(OrderServiceInterface):
"""Servicio remoto EJB o API simulado"""
def create_order(self, customer_id: str, items: list) -> dict:
# Simular llamada de red
return {"order_id": "ORD-123", "status": "created", "items": items}
def get_order(self, order_id: str) -> dict:
return {"order_id": order_id, "status": "shipped"}
class ServiceLocator:
"""Registro central para lookup de servicios"""
_services: Dict[str, Any] = {}
@classmethod
def register(cls, name: str, service: Any):
cls._services[name] = service
@classmethod
def lookup(cls, name: str) -> Any:
return cls._services.get(name)
class OrderBusinessDelegate:
"""Delegate que simplifica acceso a OrderService"""
def __init__(self):
self._service: Optional[OrderServiceInterface] = None
def _get_service(self) -> OrderServiceInterface:
if self._service is None:
self._service = ServiceLocator.lookup("OrderService")
if self._service is None:
self._service = RemoteOrderService()
return self._service
def create_order(self, customer_id: str, items: list) -> dict:
try:
return self._get_service().create_order(customer_id, items)
except Exception as e:
# Manejar excepciones remotas, retry logic, etc.
raise RuntimeError(f"Failed to create order: {e}")
def get_order(self, order_id: str) -> dict:
return self._get_service().get_order(order_id)
# Capa de presentación (equivalente servlet/controller)
class OrderController:
def __init__(self):
self.delegate = OrderBusinessDelegate()
def handle_create_order(self, customer_id: str, items: list):
result = self.delegate.create_order(customer_id, items)
return f"Order created: {result['order_id']}"
# Uso
ServiceLocator.register("OrderService", RemoteOrderService())
controller = OrderController()
print(controller.handle_create_order("CUST-001", ["item1", "item2"]))
Java
import java.util.*;
interface OrderService {
Map<String, Object> createOrder(String customerId, List<String> items);
Map<String, Object> getOrder(String orderId);
}
class RemoteOrderService implements OrderService {
public Map<String, Object> createOrder(String customerId, List<String> items) {
Map<String, Object> result = new HashMap<>();
result.put("order_id", "ORD-123");
result.put("status", "created");
result.put("items", items);
return result;
}
public Map<String, Object> getOrder(String orderId) {
Map<String, Object> result = new HashMap<>();
result.put("order_id", orderId);
result.put("status", "shipped");
return result;
}
}
class ServiceLocator {
private static final Map<String, Object> services = new HashMap<>();
public static void register(String name, Object service) { services.put(name, service); }
public static Object lookup(String name) { return services.get(name); }
}
class OrderBusinessDelegate {
private OrderService service;
private OrderService getService() {
if (service == null) {
service = (OrderService) ServiceLocator.lookup("OrderService");
if (service == null) service = new RemoteOrderService();
}
return service;
}
public Map<String, Object> createOrder(String customerId, List<String> items) {
try {
return getService().createOrder(customerId, items);
} catch (Exception e) {
throw new RuntimeException("Failed to create order: " + e.getMessage(), e);
}
}
public Map<String, Object> getOrder(String orderId) {
return getService().getOrder(orderId);
}
}
class OrderController {
private final OrderBusinessDelegate delegate = new OrderBusinessDelegate();
public String handleCreateOrder(String customerId, List<String> items) {
Map<String, Object> result = delegate.createOrder(customerId, items);
return "Order created: " + result.get("order_id");
}
}
// Uso
ServiceLocator.register("OrderService", new RemoteOrderService());
OrderController controller = new OrderController();
System.out.println(controller.handleCreateOrder("CUST-001", List.of("item1", "item2")));
JavaScript
class RemoteOrderService {
createOrder(customerId, items) {
return { order_id: 'ORD-123', status: 'created', items };
}
getOrder(orderId) {
return { order_id: orderId, status: 'shipped' };
}
}
class ServiceLocator {
static services = new Map();
static register(name, service) { this.services.set(name, service); }
static lookup(name) { return this.services.get(name); }
}
class OrderBusinessDelegate {
constructor() {
this.service = null;
}
getService() {
if (!this.service) {
this.service = ServiceLocator.lookup('OrderService') || new RemoteOrderService();
}
return this.service;
}
createOrder(customerId, items) {
try {
return this.getService().createOrder(customerId, items);
} catch (e) {
throw new Error(`Failed to create order: ${e.message}`);
}
}
getOrder(orderId) {
return this.getService().getOrder(orderId);
}
}
class OrderController {
constructor() {
this.delegate = new OrderBusinessDelegate();
}
handleCreateOrder(customerId, items) {
const result = this.delegate.createOrder(customerId, items);
return `Order created: ${result.order_id}`;
}
}
// Uso
ServiceLocator.register('OrderService', new RemoteOrderService());
const controller = new OrderController();
console.log(controller.handleCreateOrder('CUST-001', ['item1', 'item2']));
Explicación
El Business Delegate actúa como proxy y adapter entre la capa de presentación y los servicios de negocio:
- Capa de Presentación: Usa la simple interfaz del Business Delegate
- Business Delegate: Maneja lookup de servicio, caching, traducción de excepciones, y retry logic
- Service Locator: El registro central para encontrar servicios de negocio
- Business Service: La implementación remota o compleja del servicio real
Este enfoque por capas significa que el código de presentación nunca referencia directamente interfaces remotas, JNDI, o excepciones específicas de servicio. El delegate funciona en la práctica como un proxy con búsqueda, caché y traducción de errores integrados.
Variantes
| Variante | Funcionalidad adicional | Caso de uso |
|---|---|---|
| Basic | Lookup y delegación simple | Apps enterprise estándar |
| Caching | Guarda referencias de servicio en caché | Sistemas de alto rendimiento |
| Retry | Reintento automático ante fallos | Servicios remotos poco fiables |
| Async | Invocación asíncrona | Presentación no bloqueante |
Delegate con caché y reintentos
La tabla de variantes no es teórica: un delegate de producción suele guardar la referencia del servicio en caché y reintentar las llamadas remotas con backoff:
import time
class ResilientOrderDelegate:
"""Guarda la referencia del servicio y reintenta llamadas remotas."""
def __init__(self, max_retries: int = 3):
self._service = None
self._max_retries = max_retries
def _get_service(self):
if self._service is None: # se busca una vez y queda en caché
self._service = ServiceLocator.lookup("OrderService") or RemoteOrderService()
return self._service
def create_order(self, customer_id: str, items: list) -> dict:
delay = 0.05
for attempt in range(1, self._max_retries + 1):
try:
return self._get_service().create_order(customer_id, items)
except Exception as exc:
if attempt == self._max_retries:
raise RuntimeError("Servicio de pedidos no disponible tras %d intentos" % attempt) from exc
time.sleep(delay) # backoff exponencial simple
delay *= 2
Dos detalles importan aquí. La búsqueda corre una sola vez y la referencia queda en caché, así que las llamadas repetidas no pagan el coste de JNDI o del registro. Y el bucle de reintentos convierte una llamada remota inestable en un resultado o en un único error de dominio: el controller jamás ve una excepción de transporte.
Delegate vs. soluciones adyacentes
| Enfoque | Rasgo compartido | Diferencia clave |
|---|---|---|
| Facade | Simplifica una API compleja | Oculta la complejidad del subsistema, no lo remoto |
| Service Locator | Encuentra servicios | Solo un registro: sin política de caché, reintentos ni traducción de excepciones |
| Inyección de dependencias | Invierte la creación de servicios | Conecta colaboradores al arranque; sin búsqueda en runtime ni manejo de errores remotos |
| Business Delegate | Se interpone entre capas | Agrupa búsqueda, caché, reintentos y traducción para servicios remotos |
Lo que funciona
- Guarda las referencias de servicio en caché. Evita búsquedas repetidas en JNDI o en el registro.
- Traduce excepciones. Convierte excepciones remotas/de servicio en errores amigables para presentación.
- Mantén el delegate ligero. La lógica de negocio pertenece al servicio, no al delegate.
- Úsalo con Service Locator o DI. El delegate no debería fijar a fuego la creación del servicio.
- Implementa reintentos para llamadas remotas. Los fallos de red deberían resolverse sin que el usuario lo note.
Errores Comunes
- Poner lógica de negocio en el delegate. El delegate solo debería rutear y simplificar.
- No guardar referencias en caché. Las búsquedas repetidas son costosas.
- Exponer excepciones remotas a la capa de presentación. Siempre traduce a excepciones de dominio.
- Acoplamiento fuerte a una implementación específica de servicio. Usa interfaces y factories.
- Usarlo de más para servicios locales. La inyección directa es más simple cuando los servicios viven en el mismo proceso.
Ejemplos del Mundo Real
Java EE / Jakarta EE
Business Delegate fue un patrón core de J2EE. Los session beans eran accedidos vía delegates para ocultar complejidad EJB de servlets y JSPs.
Spring Framework
Aunque la inyección de dependencias de Spring reduce la necesidad de delegates manuales, las capas @Service con @Transactional a menudo actúan como business delegates entre controllers y repositories.
Microservice Gateways
Los API gateways en arquitecturas de microservicios a menudo implementan lógica de Business Delegate, agregando múltiples servicios backend en una única interfaz orientada al cliente.
Véase también
- Patrón Business Delegate — Wikipedia: definición formal y contexto J2EE.
- Core J2EE Patterns: el catálogo donde Sun documentó el patrón para aplicaciones empresariales distribuidas.
- Código complementario: el repo companion de business-delegate-pattern empaqueta estos ejemplos como archivos Python, Java y JavaScript ejecutables.
Preguntas frecuentes
Cuál es la diferencia entre Business Delegate y Facade?
Una Facade simplifica la interfaz de un subsistema complejo. Un Business Delegate media específicamente entre presentación y servicios de negocio remotos/distribuidos.
Cómo se relaciona Business Delegate con Service Locator?
El delegate a menudo usa Service Locator para encontrar el servicio de negocio real. Son patrones complementarios.
Es Business Delegate aún relevante con frameworks modernos de DI?
La necesidad de delegates manuales ha disminuido con Spring y CDI, pero el concepto vive en service layers, BFFs, y API gateways.
¿Cuál es la diferencia entre Business Delegate y un BFF (Backend for Frontend)?
Un BFF es un servicio de backend dedicado por tipo de cliente; un Business Delegate es un objeto dentro de la capa de presentación. Ambos aíslan al cliente de la complejidad del servicio: el BFF cruzando una frontera de red, el delegate dentro del mismo proceso.
¿Necesito un Business Delegate con Spring o CDI?
Normalmente no como clase separada: el contenedor ya maneja búsqueda e inyección. El patrón sigue ganando su lugar como capa de cara al servicio cuando las llamadas remotas necesitan caché, reintentos o traducción de errores.
Recursos Relacionados
Patrón Facade
Provee una interfaz simplificada a un subsistema complejo. Un patrón estructural que oculta detalles de implementación detrás de una API limpia.
PatternPatrón Proxy
Proporciona un sustituto o marcador de posición para otro objeto para controlar el acceso a él. Un patrón estructural para control de acceso, carga perezosa y logging.
PatternPatrón Context Object: Ejemplos
Reduce la proliferación de parámetros pasando un único objeto de contexto por la cadena de llamadas. Ejemplos en Python, Java y JavaScript.
PatternIntercepting Filter: Filtros de Petición Reutilizables
Compón responsabilidades transversales en una cadena de filtros reutilizables que interceptan peticiones y respuestas, permitiendo lógica de preprocesamiento y postprocesamiento compartida.
PatternPatrón Domain Event
Captura y publica ocurrencias importantes dentro de un modelo de dominio para desacoplar efectos secundarios de la lógica de negocio central y habilitar workflows reactivos.
GuideGuía Práctica de Design Patterns
Elige el design pattern correcto para cada problema: Singleton, Factory, Observer, Strategy y más, con código ejecutable y trade-offs honestos.