Patrón decorador en servicios de pagos

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

Resumen

Aplicar el patrón decorador en Python con protocolos tipados aporta flexibilidad y control. Aquí verás cómo envolver un PaymentService con lógica de logging sin tocar el comportamiento original, usando typing.Protocol, composición con wrap y una implementación limpia con @dataclass.

¿Cómo aplicar el patrón decorador al PaymentService paso a paso?

Para extender un servicio de pagos sin modificarlo, se define una interfaz con Protocol, se implementa la clase principal conforme a esa interfaz y luego se crea un decorador que delega en el servicio real, añadiendo logging.

  • Crear el protocolo del servicio con typing.Protocol.
  • Hacer que la clase PaymentService implemente ese protocolo.
  • Definir un protocolo para los decoradores con el atributo wrap.
  • Implementar un decorador de logging que imprime inicio y fin, y delega la lógica.
  • Instanciar el decorador en main envolviendo el servicio real.
# service_protocol.py
from typing import Protocol

class PaymentServiceProtocol(Protocol):
    def process_transaction(self, customer_data, payment_data): ...
    def process_refund(self, transaction_id: str): ...
    def setup_recurring(self, customer_data, payment_data): ...

¿Qué protocolos y clases intervienen en el diseño?

Se definen dos abstracciones clave. Primero, el protocolo del servicio que actúa como contrato: especifica firmas, no comportamiento. Segundo, el protocolo del decorador que replica esas firmas y agrega el atributo wrap tipado como PaymentServiceProtocol para delegar la ejecución. Esto asegura sustitución transparente y facilita pruebas.

# decorator_protocol.py
from typing import Protocol
from service_protocol import PaymentServiceProtocol

class PaymentServiceDecoratorProtocol(Protocol):
    wrap: PaymentServiceProtocol

    def process_transaction(self, customer_data, payment_data): ...
    def process_refund(self, transaction_id: str): ...
    def setup_recurring(self, customer_data, payment_data): ...
  • Protocol: interfaz estructural que define métodos esperados.
  • wrap: composición para delegar en el servicio real.
  • Firmas replicadas: garantizan compatibilidad con el contrato original.
  • Importaciones mínimas: solo lo necesario para tipar y delegar.

¿Cómo mejora el logging sin alterar la lógica original?

El decorador PaymentServiceLogging implementa el protocolo del decorador, usa @dataclass para su construcción y añade prints de inicio y fin alrededor de cada método. La clave es llamar a self.wrap con los mismos parámetros y retornar la respuesta original, manteniendo el comportamiento intacto.

# logging_service.py
from dataclasses import dataclass
from decorator_protocol import PaymentServiceDecoratorProtocol
from service_protocol import PaymentServiceProtocol

@dataclass
class PaymentServiceLogging(PaymentServiceDecoratorProtocol):
    wrap: PaymentServiceProtocol

    def process_transaction(self, customer_data, payment_data):
        print("start process transaction")
        response = self.wrap.process_transaction(customer_data, payment_data)
        print("finish process transaction")
        return response

    def process_refund(self, transaction_id: str):
        print(f"start process refund: transaction_id={transaction_id}")
        response = self.wrap.process_refund(transaction_id)
        print("finish process refund")
        return response

    def setup_recurring(self, customer_data, payment_data):
        print("start setup recurring")
        response = self.wrap.setup_recurring(customer_data, payment_data)
        print("finish setup recurring")
        return response

¿Cómo se instancia el decorador en main?

# main.py (fragmento)
from payment_service import PaymentService  # implementación existente
from logging_service import PaymentServiceLogging

service = PaymentService(...)
service = PaymentServiceLogging(wrap=service)

service.process_transaction(customer_data, payment_data)
service.process_refund("1234")  # si el servicio real no implementa refund, fallará.

En la demostración, el intento de refund sirve para ilustrar el flujo decorado, aunque el servicio real no tenga ese procesador. La idea central se mantiene: extender funcionalidad (logging y depuración) sin modificar el código del servicio.

¿Qué habilidades y keywords refuerzas con este patrón?

  • Diseño con Protocol como interfaz estructural.
  • Abstracción y contrato de métodos.
  • Composición con wrap para delegación.
  • Extensibilidad sin modificar comportamiento original.
  • Logging y debugging de transacciones.
  • Uso de @dataclass para instanciación clara.

¿De qué otras formas aplicarías el patrón decorador en tu PaymentService o en otras capas? Comparte tus ideas y casos en los comentarios.