Patrón Factory para procesar pagos con match

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

Resumen

Aplicar patrón factory en Python permite encapsular la lógica de creación de objetos y tomar decisiones limpias y reutilizables. Aquí se muestra cómo elegir entre Stripe, offline y local según el PaymentData y su PaymentType, usando enum, match, static method y un protocol para el procesador. Además, se integra en un class method con tipado Self y manejo de errores con ValueError.

¿Qué problema resuelve el patrón factory en pagos?

El objetivo es centralizar la decisión de qué procesador instanciar según la información de pago. El PaymentData ahora incluye un tipo mediante un enum de Python con valores: offline u online. El tipo por defecto es online, lo que aporta contexto sobre cómo cobrar.

  • Si el tipo es offline, retorna el procesador OfflinePaymentProcessor.
  • Si el tipo es online y la moneda es USD, retorna StripePaymentProcessor.
  • Si el tipo es online y la moneda no es USD, retorna LocalPaymentProcessor.
  • Se define un caso por defecto que lanza ValueError para tipos no soportados.

Así, la lógica de decisión queda encapsulada y puede reutilizarse en todo el servicio.

¿Cómo implementar PaymentProcessorFactory con match en Python?

La clase PaymentProcessorFactory expone un static method createPaymentProcessor que recibe paymentData y devuelve un PaymentProcessorProtocol. La decisión se implementa con el operador de pattern matching match.

from commons import PaymentData, PaymentType
from processors import (
    PaymentProcessorProtocol,
    OfflinePaymentProcessor,
    StripePaymentProcessor,
    LocalPaymentProcessor,
)

class PaymentProcessorFactory:
    @staticmethod
    def createPaymentProcessor(paymentData: PaymentData) -> PaymentProcessorProtocol:
        match paymentData.type:
            case PaymentType.OFFLINE:
                return OfflinePaymentProcessor()
            case PaymentType.ONLINE:
                match paymentData.currency:
                    case "USD":
                        return StripePaymentProcessor()
                    case _:
                        return LocalPaymentProcessor()
            case _:
                # Caso ejemplo para tipos no soportados
                raise ValueError("Tipo de pago no soportado")

Puntos clave: - PaymentType es un enum con valores offline y online. - Se usa match con case y _ como caso por defecto. - El método retorna un PaymentProcessorProtocol, asegurando contrato de integración. - Se lanza ValueError para señalizar validaciones fallidas.

¿Cómo integrarlo en el servicio con createWithPaymentProcessor?

Se añade un class method que, a partir del paymentData, pide al factory el procesador adecuado y crea la instancia del servicio con los demás atributos que llegan como kwargs: notifier, customer validator, payment validator y logger.

from typing import Self
from factory import PaymentProcessorFactory

class PaymentService:
    @classmethod
    def createWithPaymentProcessor(cls, paymentData: PaymentData, **kwargs) -> Self:
        try:
            processor = PaymentProcessorFactory.createPaymentProcessor(paymentData)
            return cls(payment_processor=processor, **kwargs)
        except ValueError as e:
            print("Error creando la clase")
            raise e
  • Se utiliza Self para indicar que retorna una instancia de la misma clase.
  • kwargs actúa como diccionario con los atributos restantes del servicio.
  • El try/except captura ValueError lanzada por el factory.

¿Cómo usarlo desde main con PaymentData de ejemplo?

El PaymentData incluye monto, fuente y moneda. Si la moneda es USD y el tipo por defecto es online, se selecciona Stripe.

from commons import PaymentData

paymentData = PaymentData(
    amount=100,
    source="stock visa",
    currency="USD",  # type por defecto: online
)

service = PaymentService.createWithPaymentProcessor(
    paymentData,
    notifier=notifier,
    customer_validator=customer_validator,
    payment_validator=payment_validator,
    logger=logger,
)

¿Qué habilidades y conceptos aplicas aquí?

  • Patrón factory: encapsular creación y aislar reglas de negocio.
  • Enums: PaymentType con offline y online.
  • Match/case en Python para decisiones claras y anidadas.
  • Protocolos: PaymentProcessorProtocol como contrato de integración.
  • Métodos estáticos y de clase: static method para crear, class method para orquestar.
  • Tipado avanzado: uso de Self y tipos explícitos.
  • Manejo de errores: ValueError para entradas no soportadas.
  • Composición con kwargs: pasar notifier, customer validator, payment validator y logger al constructor.
  • Depuración: uso de breakpoint para verificar el flujo y la instancia seleccionada.

¿De qué otra forma aplicarías patrón factory en este proyecto? Comparte tus ideas en comentarios.