Builder pattern para servicios de pagos

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

Resumen

Aplicar el patrón builder en Python permite construir un servicio de pagos flexible, seguro y fácil de mantener. Aquí verás cómo crear un PaymentServiceBuilder con atributos opcionales, métodos encadenables, validaciones robustas y la integración con un factory para seleccionar el procesador de pago correcto y un notificador por email o SMS.

¿Qué problema resuelve el builder pattern en un servicio de pagos?

El escenario requiere muchas configuraciones previas a la instanciación del servicio. El builder organiza estos pasos, hace explícitas las dependencias y evita errores de inicialización.

  • Muchos pasos previos: procesador, notificador, validadores y logger.
  • Atributos opcionales con valor None hasta completar la configuración.
  • Encadenamiento de métodos que retornan self para una construcción fluida.
  • Validación previa con un método build que asegura dependencias completas.
  • Combinación de patrones: factory para seleccionar el payment processor y builder para orquestar la construcción.

¿Cómo implementar el builder del PaymentService paso a paso?

La clase del builder replica los atributos del servicio final, pero los define como opcionales con valor por defecto None. Se usa data class para simplificar.

¿Cómo se declaran atributos opcionales con data class?

from dataclasses import dataclass
from typing import Optional, Self

# Protocolos/clases referenciadas como tipos (ya importadas en tu proyecto)
# PaymentProcessorProtocol, TransactionLogger, PaymentDataValidator, CustomerDataValidator

@dataclass
class PaymentServiceBuilder:
    logger: Optional[TransactionLogger] = None
    payment_validator: Optional[PaymentDataValidator] = None
    customer_validator: Optional[CustomerDataValidator] = None
    payment_processor: Optional[PaymentProcessorProtocol] = None
    notifier: Optional[object] = None  # EmailNotifier o SMSNotifier
  • Mismos atributos que el servicio final pero con Optional y None.
  • Importaciones de protocolos y clases necesarias para tipado y funcionamiento.

¿Cómo encadenar métodos set y qué se instancia directo?

Logger y validadores no dependen de abstracciones externas, se instancian directo.

def setLogger(self) -> Self:
    self.logger = TransactionLogger()
    return self

def setPaymentValidator(self) -> Self:
    self.payment_validator = PaymentDataValidator()
    return self

def setCustomerValidator(self) -> Self:
    self.customer_validator = CustomerDataValidator()
    return self
  • Cada set retorna self: permite encadenar llamadas.
  • Instanciación directa: sin lógica adicional.

¿Cómo integrar factory y notificador según datos?

Para el procesador se usa un factory; para el notificador, reglas con if.

# from commons import PaymentData
# from factory import PaymentProcessorFactory

def setPaymentProcessor(self, payment_data: PaymentData) -> Self:
    self.payment_processor = PaymentProcessorFactory.create_payment_processor(payment_data)
    return self

# from commons import CustomerData
# importa EmailNotifier y SMSNotifier según tu estructura

def setNotifier(self, customer_data: CustomerData) -> Self:
    contact = customer_data.contact_info
    if contact.email:
        self.notifier = EmailNotifier()
        return self
    if contact.phone_number:
        self.notifier = SMSNotifier(gateway="my custom gateway")
        return self
    raise ValueError("No se puede seleccionar clase de notificación.")
  • Selección por factory: decide el mejor procesador según el tipo de pago.
  • Reglas de notificación: email o SMS según contact info.
  • Excepción temprana: si no hay email ni teléfono.

¿Qué valida build y cómo maneja errores?

El método build verifica que las dependencias críticas estén presentes antes de instanciar el servicio. Si falta algo, informa exactamente qué falta.

# from service import PaymentService

def build(self) -> PaymentService:
    required = [
        self.payment_processor,
        self.notifier,
        self.customer_validator,
        self.payment_validator,
        self.logger,
    ]

    if not all(required):
        # pares (nombre, valor) para identificar faltantes
        pairs = [
            ("payment_processor", self.payment_processor),
            ("notifier", self.notifier),
            ("customer_validator", self.customer_validator),
            ("payment_validator", self.payment_validator),
            ("logger", self.logger),
        ]
        missing = [name for name, value in pairs if value is None]
        raise ValueError(f"missing dependencies: {missing}")

    # El editor puede advertir por tipos Optional; ya fueron validados arriba.
    return PaymentService(
        payment_processor=self.payment_processor,
        payment_validator=self.payment_validator,
        customer_validator=self.customer_validator,
        notifier=self.notifier,
        logger=self.logger,
    )
  • Chequeo con all: si hay None en requeridos, falla la validación.
  • Lista missing: indica en claro qué dependencia falta.
  • Instanciación final segura: solo ocurre con dependencias completas.

¿Cómo usar el builder y qué ocurre si falta un set?

Encadena sets y finaliza con build. Si omites un set requerido, build lanza ValueError con la dependencia faltante.

# from builder import PaymentServiceBuilder
# payment_data y customer_data ya preparados

builder = PaymentServiceBuilder()
service = (
    builder
    .setLogger()
    .setPaymentValidator()
    .setCustomerValidator()
    .setPaymentProcessor(payment_data)
    .setNotifier(customer_data)
    .build()
)

# Si omites, por ejemplo, .setPaymentValidator():
# ValueError: missing dependencies: ['payment_validator']
  • Encadenamiento fluido: claridad y orden en la configuración.
  • Errores claros: facilitan depurar con breakpoints y corregir rápido.
  • Extensión sugerida: añade sets para procesadores de refund y recurring.

¿Te animas a ir más allá? agrega un nuevo método en el builder que decore el comportamiento del PaymentService usando un decorator visto antes y comparte tu solución en comentarios.