Saltar al contenidoJMRG
Todas las entradas

Patrón Facade: Simplificando Sistemas Complejos

design-patternsstructuralfacade

¿Qué es el Patrón Facade?

El patrón Facade proporciona una interfaz simplificada a un conjunto complejo de clases, biblioteca o framework. Oculta la complejidad de los subsistemas tras una única clase que expone solo las operaciones necesarias para el cliente.

classDiagram
    class Facade {
        +operation()
    }

    class SubsystemA {
        +operationA()
    }

    class SubsystemB {
        +operationB()
    }

    class SubsystemC {
        +operationC()
    }

    class Client

    Client --> Facade
    Facade --> SubsystemA
    Facade --> SubsystemB
    Facade --> SubsystemC

Cuándo Usar Facade

  • Simplificar el acceso a un sistema complejo
  • Desacoplar el código cliente de los subsistemas
  • Proporcionar un punto de entrada unificado
  • Organizar subsistemas en capas

El Problema

En un sistema e-commerce, tenemos múltiples servicios independientes que el cliente debe coordinar:

// Sin Facade - El cliente debe coordinar todo manualmente
const inventory = new InventoryService();
const payment = new PaymentService();
const shipping = new ShippingService();
const notification = new NotificationService();

// Verificar stock
for (const item of items) {
  const inStock = await inventory.checkStock(item.productId, item.quantity);
  if (!inStock) throw new Error('Sin stock');
  reservations.push(await inventory.reserveStock(item.productId, item.quantity));
}

// Calcular envío
const shippingOptions = await shipping.calculateShipping(address, items);
const selectedShipping = shippingOptions[0];

// Procesar pago
const authId = await payment.authorize(total, 'EUR', paymentMethod);
await payment.capture(authId);

// ... más pasos complejos ...

La Solución: Facade

1. Identificar los Subsistemas

class InventoryService {
  async checkStock(productId: string, quantity: number): Promise<boolean> { }
  async reserveStock(productId: string, quantity: number): Promise<string> { }
  async confirmReservation(reservationId: string): Promise<void> { }
  async releaseReservation(reservationId: string): Promise<void> { }
}

class PaymentService {
  async authorize(amount: number, currency: string, method: string): Promise<string> { }
  async capture(authorizationId: string): Promise<string> { }
  async voidAuthorization(authorizationId: string): Promise<void> { }
}

class ShippingService {
  async calculateShipping(address: Address, items: OrderItem[]): Promise<ShippingOption[]> { }
  async createShipment(orderId: string, address: Address, carrier: string): Promise<string> { }
}

class NotificationService {
  async sendOrderConfirmation(email: string, orderId: string): Promise<void> { }
  async sendPaymentReceipt(email: string, amount: number): Promise<void> { }
}

2. Definir la Interfaz Simplificada

El cliente solo necesita una operación:

interface OrderRequest {
  userId: string;
  email: string;
  items: Array<{ productId: string; quantity: number; price: number }>;
  shippingAddress: Address;
  paymentMethod: string;
}

interface OrderResult {
  orderId: string;
  total: number;
  shippingCost: number;
  estimatedDelivery: Date;
  trackingId?: string;
}

3. Implementar el Facade

El Facade coordina todos los subsistemas:

class OrderFacade {
  private inventory = new InventoryService();
  private payment = new PaymentService();
  private shipping = new ShippingService();
  private notification = new NotificationService();

  async placeOrder(request: OrderRequest): Promise<OrderResult> {
    const reservations: string[] = [];
    let authorizationId: string | null = null;

    try {
      // 1. Verificar stock y reservar
      for (const item of request.items) {
        const inStock = await this.inventory.checkStock(item.productId, item.quantity);
        if (!inStock) {
          throw new Error(`Producto ${item.productId} sin stock`);
        }
        const reservationId = await this.inventory.reserveStock(item.productId, item.quantity);
        reservations.push(reservationId);
      }

      // 2. Calcular totales
      const subtotal = request.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
      const shippingOptions = await this.shipping.calculateShipping(
        request.shippingAddress,
        request.items
      );
      const shippingCost = shippingOptions[0].cost;
      const total = subtotal + shippingCost;

      // 3. Procesar pago
      authorizationId = await this.payment.authorize(total, 'EUR', request.paymentMethod);
      await this.payment.capture(authorizationId);

      // 4. Confirmar reservas
      for (const reservationId of reservations) {
        await this.inventory.confirmReservation(reservationId);
      }

      // 5. Crear envío
      const orderId = `ORD-${Date.now()}`;
      const trackingId = await this.shipping.createShipment(
        orderId,
        request.shippingAddress,
        shippingOptions[0].carrier
      );

      // 6. Enviar notificaciones
      await this.notification.sendOrderConfirmation(request.email, orderId);
      await this.notification.sendPaymentReceipt(request.email, total);

      return {
        orderId,
        total,
        shippingCost,
        estimatedDelivery: shippingOptions[0].estimatedDelivery,
        trackingId,
      };
    } catch (error) {
      // Rollback en caso de error
      for (const reservationId of reservations) {
        await this.inventory.releaseReservation(reservationId);
      }
      if (authorizationId) {
        await this.payment.voidAuthorization(authorizationId);
      }
      throw error;
    }
  }
}

4. Usar el Facade

// Con Facade - Una sola llamada
const facade = new OrderFacade();

const result = await facade.placeOrder({
  userId: 'user_123',
  email: '[email protected]',
  items: [
    { productId: 'PROD_001', quantity: 2, price: 29.99 },
  ],
  shippingAddress: {
    street: 'Calle Principal 123',
    city: 'Madrid',
    country: 'España',
    postalCode: '28001',
  },
  paymentMethod: 'card_visa_****1234',
});

console.log(`Pedido ${result.orderId} completado!`);

Beneficios del Patrón Facade

Beneficio Descripción
Simplicidad El cliente solo conoce una interfaz
Desacoplamiento Cambios en subsistemas no afectan al cliente
Mantenibilidad Lógica de coordinación centralizada
Testabilidad Fácil de mockear para tests

Resumen

Aspecto Descripción
Propósito Simplificar acceso a sistemas complejos
Componentes Facade, Subsystems, Client
Ventaja Desacoplamiento, punto de entrada único
Desventaja Puede convertirse en "god object" si crece demasiado

Conclusión

El patrón Facade es esencial para simplificar sistemas complejos y crear APIs limpias. Es ampliamente usado en librerías y frameworks para ocultar la complejidad interna.

En el próximo artículo exploraremos el Patrón State, que permite cambiar el comportamiento de un objeto según su estado interno.


Basado en "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).