Patrones idiomáticos y modernización en Dart 3

Los patrones de diseño clásicos (GoF) han sido redefinidos por la naturaleza del lenguaje. En lugar de recurrir a la pesada jerarquía de clases y al exceso de verbosidad de Java, en Dart utilizamos capacidades nativas como sealed classes, factory constructors y extension methods para implementar lógica compleja con menos ruido.

Estos patrones idiomáticos son, en esencia, versiones optimizadas de los diseños tradicionales que aprovechan el sistema de tipos robusto de Dart para garantizar la seguridad en tiempo de compilación. Si intentas implementar un patrón Decorator clásico usando herencia y delegación en lugar de una extension, estarás añadiendo complejidad innecesaria. Si usas una clase abstracta normal en lugar de una sealed class para implementar un State Machine, pierdes la capacidad de que el compilador te avise si olvidas manejar un estado en un switch.

Este enfoque es ideal cuando necesitas modelar flujos de datos complejos (como procesos de pago o estados de una conexión), manejar errores sin recurrir a la captura de excepciones de alto nivel, o extender la funcionalidad de clases de librerías externas sin modificarlas. Si aplicas estos patrones de forma incorrecta, ignorando el sistema de tipos a favor de la herencia tradicional, terminarás con un código difícil de mantener y con errores de lógica que solo aparecerán en tiempo de ejecución.

// Implementación de un sistema de procesamiento de pedidos robusto
// que integra Result, State Machine, Factory, Strategy y Decorator.

// 1. Patrón Result (Monad-like) usando Sealed Classes para manejo de errores
sealed class Result<T> {
  const Result();
}

class Success<T> extends Result<T> {
  final T value;
  Success(this.value);
}

class Failure<T> extends Result<T> {
  final String error;
  Failure(this.error);
}

// 2. Patrón State Machine mediante Sealed Classes para el estado del pedido
sealed class OrderStatus {
  const OrderStatus();
}

class Pending extends OrderStatus {}
class Shipped extends OrderStatus {
  final String trackingCode;
  Shipped(this.trackingCode);
}
class Cancelled extends OrderStatus {
  final String reason;
  Cancelled(this.reason);
}

// 3. El modelo de datos
class Order {
  final double amount;
  final String id;

  Order(this.amount, this.id);
}

// 4. Patrón Decorator idiomático mediante Extension Methods
extension OrderDiscountDecorator on Order {
  Order withSeasonalDiscount() => Order(amount * 0.9, id);
}

// 5. Patrón Strategy usando typedef para lógica de ordenamiento
typedef OrderSorter = int Function(Order a, Order b);

// 6. Patrón Factory Method para proporcionar procesadores de pago
abstract class PaymentProcessor {
  factory PaymentProcessor(String type) {
    return switch (type) {
      'stripe' => StripeProcessor(),
      'paypal' => PayPalProcessor(),
      _ => throw ArgumentError('Procesador no soportado'),
    };
  }
  Future<Result<String>> process(double amount);
}

class StripeProcessor implements PaymentProcessor {
  @override
  Future<Result<String>> process(double amount) async {
    // Simulación de llamada a API
    return Success("Stripe: Pago de \$$amount completado");
  }
}

class PayPalProcessor implements PaymentProcessor {
  @override
  Future<Result<String>> process(double amount) async {
    return Failure("PayPal: Saldo insuficiente");
  }
}

// --- Ejecución Principal ---

void main() async {
  final orders = [
    Order(100.0, "ORD-001"),
    Order(250.0, "ORD-002"),
    Order(50.0, "ORD-003"),
  ];

  // Uso de Strategy para ordenar por monto
  final sorter = (Order a, Order b) => a.amount.compareTo(b.amount);
  orders.sort(sorter);

  print("--- Procesando Pedidos ---");

  for (var order in orders) {
    // Uso de Decorator (Extension)
    final discountedOrder = order.withSeasonalDiscount();
    
    // Uso de Factory para obtener el procesador
    final processor = PaymentProcessor('stripe');
    
    final result = await processor.process(discountedOrder.amount);

    // Uso de State Machine + Pattern Matching (Exhaustive Switch)
    // Aquí simulamos que el pedido pasa a estado Shipped tras el pago
    final OrderStatus currentStatus = Shipped("TRK-999");

    final statusMessage = switch (currentStatus) {
      Pending() => "Esperando pago...",
      Shipped(var code) => "En camino con código: $code",
      Cancelled(var reason) => "Cancelado por: $reason",
    };

    // Manejo del Result mediante Pattern Matching
    final paymentMessage = switch (result) {
      Success(var msg) => "✅ $msg",
      Failure(var err) => "❌ Error: $err",
    };

    print("Pedido ${order.id} | Pago: $paymentMessage | Status: $statusMessage");
  }
}

Desglose técnico

En el ejemplo, la clase Result<T> utiliza sealed para garantizar que cuando uses un switch para evaluar el resultado del pago, el compilador te obligue a manejar tanto Success como Failure. Si intentaras usar una clase abstract normal, el compilador no sabría que existen solo dos posibilidades, rompiendo la seguridad de la lógica de negocio.

El PaymentProcessor emplea un Factory Constructor. Observa que el constructor no devuelve una instancia de la clase base, sino que utiliza una expresión switch para retornar implementaciones concretas (StripeProcessor o PayPalProcessor). Esto encapsula la lógica de instanciación y evita que el cliente deba conocer las implementaciones subyacentes.

Para el patrón Decorator, en lugar de crear una clase DiscountedOrder que envuelva a Order, simplemente extendemos la funcionalidad mediante extension OrderDiscountDecorator on Order. Esto es mucho más eficiente en Dart porque no crea nuevos objetos en el heap para cada decoración, sino que simplemente inyecta un método en el tipo original.

Finalmente, la lógica de ordenamiento utiliza un typedef. Esto transforma el patrón Strategy de una estructura compleja de interfaces en una simple firma de función, aprovechando que en Dart las funciones son objetos de primera clase.

El error frecuente

Un error común al implementar State Machines es usar clases normales en lugar de sealed. Si defines OrderStatus como una abstract class estándar:

abstract class OrderStatus {}
class Pending extends OrderStatus {}
class Shipped extends OrderStatus {}

Y luego intentas usar un switch para procesar el estado:

// Si olvidas el caso 'Cancelled'
final message = switch (status) {
  Pending() => "Espera",
  Shipped() => "En camino",
};

El compilador no te dará error durante la compilación. Sin embargo, si en tiempo de ejecución el estado es Cancelled, la aplicación lanzará una excepción de runtime. Al usar sealed, el compilador detecta la falta del caso Cancelled y te obliga a manejarlo antes de que el código llegue siquiera a ejecutarse.

115

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio