Segregación de interfaces en procesadores de pagos

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

Resumen

Aplicar el principio de segregación de interfaces en un procesador de pagos evita dependencias innecesarias y errores en tiempo de ejecución. Aquí verás cómo separar responsabilidades en protocolos específicos para cobros, reembolsos y recurrencias, con ejemplos prácticos usando Stripe y un procesador offline. La meta: clases que solo dependan de lo que realmente usan.

¿Cómo aplicar el principio de segregación de interfaces en un procesador de pagos?

El punto de partida es un procesador que ahora incluye dos métodos nuevos: reembolso y crear recurrencia. Además, se añadió un procesador de pagos offline que solo imprime el cobro, simulando efectivo. Al no tener pasarela, no puede reembolsar ni crear recurrencias, por lo que lanzar excepciones en esos métodos revela la violación al principio: una clase no debería depender de métodos que no puede implementar.

  • Se cambia el retorno de process transaction para no depender de un charge de Stripe. Ahora se define un resultado con: estatus exitoso o fallido, monto, ID de transacción y mensaje de la pasarela.
  • El procesador offline no soporta refund ni recurring: levantar excepciones lo hace evidente y guía la refactorización.

¿Qué problema revela el procesador de pagos offline?

Cuando el offline processor implementa métodos de reembolso y recurrencia que no puede cumplir, se obliga a los clientes a depender de funciones inútiles. Eso contraviene el principio de segregación de interfaces y dificulta el mantenimiento.

¿Qué protocolos separar: cobro, reembolso y recurrencia?

La solución es crear tres protocolos con un único propósito cada uno: uno para cobrar, otro para reembolsar y otro para recurrencia. Así, cada clase implementa solo lo que necesita.

from typing import Protocol, Optional

class PMProcessorProtocol(Protocol):
    def process_transaction(self, amount: int) -> dict: ...

class RefundPaymentProtocol(Protocol):
    def refund_payment(self, transaction_id: str) -> None: ...

class RecurringPaymentProtocol(Protocol):
    def create_recurring(self, amount: int, interval: str) -> None: ...

¿Qué cambios de diseño y código se realizan para Stripe y offline?

En Stripe, se implementan los tres métodos porque la pasarela soporta cobros, reembolsos y recurrencias. El procesador offline solo implementa cobros.

  • Stripe: hereda de los tres protocolos. Soporta todo el flujo.
  • Offline: hereda solo de PMProcessorProtocol. No tiene reembolso ni recurrencia.
class StripeProcessor(PMProcessorProtocol, RefundPaymentProtocol, RecurringPaymentProtocol):
    def process_transaction(self, amount: int) -> dict:
        # lógica real de Stripe
        return {"status": "success", "amount": amount}

    def refund_payment(self, transaction_id: str) -> None:
        # refund real en Stripe
        ...

    def create_recurring(self, amount: int, interval: str) -> None:
        # plan/recurrencia en Stripe
        ...

class OfflineProcessor(PMProcessorProtocol):
    def process_transaction(self, amount: int) -> dict:
        print(f"Cobrando en efectivo: {amount}")
        return {"status": "success", "amount": amount}

Nota: se menciona renombrar la clase base para cumplir un estándar: PM Processor Protocol. El objetivo es claridad y consistencia en nombres.

¿Cómo adaptar el servicio para refund y recurrencia opcionales?

Como los métodos de refund y recurring ya no están en el payment processor general, el service agrega dos atributos opcionales: refund_processor y recurring_processor. Se valida None antes de llamar y, de no existir, se lanza una excepción clara.

  • Agregar atributos opcionales: Optional[RefundPaymentProtocol] y Optional[RecurringPaymentProtocol].
  • Validar antes de ejecutar: si es None, lanzar Exception con mensajes explícitos.
  • Instanciación flexible: con Stripe, un solo procesador puede cubrir los tres flujos; con offline, no se pasan refund ni recurring.
class PaymentService:
    def __init__(
        self,
        payment_processor: PMProcessorProtocol,
        refund_processor: Optional[RefundPaymentProtocol] = None,
        recurring_processor: Optional[RecurringPaymentProtocol] = None,
    ) -> None:
        self.payment_processor = payment_processor
        self.refund_processor = refund_processor
        self.recurring_processor = recurring_processor

    def refund(self, transaction_id: str) -> None:
        if not self.refund_processor:
            raise Exception("this processor does not support refunds")
        self.refund_processor.refund_payment(transaction_id)

    def create_recurring(self, amount: int, interval: str) -> None:
        if not self.recurring_processor:
            raise Exception("This processor does not support recurrent")
        self.recurring_processor.create_recurring(amount, interval)

Instanciación con Stripe como único procesador para todo:

stripe = StripeProcessor()
service = PaymentService(
    payment_processor=stripe,
    refund_processor=stripe,
    recurring_processor=stripe,
)

Instanciación con offline solo para cobros en efectivo:

offline = OfflineProcessor()
service_cash = PaymentService(
    payment_processor=offline,
    # sin refund ni recurring para mantener la segregación
)

Palabras clave y habilidades puestas en práctica: - Principio de segregación de interfaces: evitar dependencias a métodos no usados. - Protocolos especializados: PMProcessorProtocol, RefundPaymentProtocol, RecurringPaymentProtocol. - Tipos opcionales y validación de None: robustez en el service. - Excepciones claras: mensajes orientados a capacidades del procesador. - Independencia de pasarela: retorno genérico en process transaction (estatus, monto, id, mensaje).

¿Qué otras mejoras aplicarías para reforzar la segregación de interfaces en este caso de uso de procesadores de pago? Comparte tus ideas y ejemplos en comentarios.