Chain of Responsibility en servicios de pagos

Clase 26 de 27Curso de Patrones de Diseño y SOLID en Python

Resumen

Aplicar el patrón Chain of Responsibility a un servicio de pagos en Python mejora la organización de validaciones y facilita la extensión. Aquí verás cómo encapsular datos con Pydantic, crear un chain handler abstracto con ABC y dataclass, implementar un CustomerHandler y ajustar el servicio y el builder para validar en cadena.

¿Cómo estructurar el request y por qué usar Pydantic?

Encapsular en un solo objeto los datos que participan en la validación simplifica la firma de métodos y estandariza la entrada. Se define una clase request que hereda de BaseModel para asegurar tipos y validación básica.

  • Encapsulación en un único request para customer y payment.
  • Uso de Pydantic BaseModel: tipado fuerte y validación declarativa.
  • Menos acoplamiento: validadores reciben el mismo contrato.
from pydantic import BaseModel

# Declarada en el módulo commons
class Request(BaseModel):
    customer_data: CustomerData
    payment_data: PaymentData

¿Cómo definir el chain handler abstracto con ABC y dataclass?

La cadena requiere un eslabón común con comportamiento compartido: establecer el siguiente handler y un método handle obligado. Se combina ABC para el contrato, dataclass para el atributo next_handler, y Self para tipos recursivos. El método set_next retorna el propio siguiente eslabón para encadenar con fluidez.

  • Clase abstracta con @abstractmethod para handle.
  • Atributo next_handler opcional para el siguiente eslabón.
  • set_next uniforme en toda la cadena.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional, Self
from commons import Request

@dataclass
class ChainHandler(ABC):
    next_handler: Optional[Self] = None

    def set_next(self, handler: Self) -> Self:
        self.next_handler = handler
        return handler

    @abstractmethod
    def handle(self, request: Request) -> None:
        ...

¿Cómo implementar CustomerHandler y conectar la cadena en el builder?

Este eslabón valida el customer con un validador existente. Si todo es correcto, delega al siguiente handler. Si ocurre un error, lo propaga. Se usa try/except porque el validador lanza excepciones cuando hay datos inválidos.

  • Delegación al siguiente eslabón solo si existe next_handler.
  • Propagación de excepciones para detener el flujo ante fallas.
  • Separación de responsabilidades: cada handler valida una cosa.
from commons import Request
from validators.handler import ChainHandler
from Customer import CustomerValidator  # Validador existente

class CustomerHandler(ChainHandler):
    def handle(self, request: Request) -> None:
        validator = CustomerValidator()
        try:
            validator.validate(request.customer_data)
            if self.next_handler:
                self.next_handler.handle(request)
        except Exception as exc:
            print("Fallo en validación de customer.")
            raise exc

¿Cómo se adapta el servicio y el protocolo?

El servicio deja de depender de múltiples validadores y recibe un único ChainHandler. Construye el request y ejecuta la cadena. Ante error, imprime y relanza la excepción para frenar el proceso.

from commons import Request

# Dentro del servicio
req = Request(customer_data=customer_data, payment_data=payment_data)
try:
    self.validator.handle(req)
except Exception as exc:
    print("Fallo en las validaciones.")
    raise exc
  • Protocolo actualizado: depende de ChainHandler, no de una lista de validadores.
  • Implementación alineada: un único punto de entrada handle.
  • Flujo claro: construir Request y ejecutar la cadena.

¿Cómo configurar la cadena en el builder?

El builder expone un método para armar la cadena. El primer eslabón se guarda como self.validator. El método retorna self para encadenar configuraciones y el build inyecta el validador en el servicio.

from typing import Optional, Self
from validators.handler import ChainHandler
from validators.customer_handler import CustomerHandler

class PaymentServiceBuilder:
    validator: Optional[ChainHandler] = None

    def set_chain_of_validations(self) -> Self:
        ch1 = CustomerHandler()
        ch2 = CustomerHandler()  # Ejemplo: encadenar otro eslabón
        ch1.set_next(ch2)
        self.validator = ch1
        return self

    def build(self) -> PaymentService:
        return PaymentService(validator=self.validator)
  • Método fluido: retorna self para continuar la configuración.
  • Primer eslabón: punto de entrada de la cadena.
  • Opcionalidad: validator puede ser None hasta configurarlo.

¿Cómo implementarías el handler para payment_data y cómo encadenarías toda la validación en el builder? Comparte tu enfoque y decisiones en los comentarios.