Cómo aplicar SRP en un procesador de pagos con Stripe

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

Resumen

Aplicar el principio de responsabilidad única (SRP) en Python con Stripe no solo ordena el código: también hace el flujo de cobro más claro, testeable y mantenible. Aquí verás cómo se pasó de un PaymentProcessor monolítico a un diseño con validadores, servicio orquestador y componentes dedicados para notificación y registro, usando data class, type hints y manejo de excepciones.

¿Qué problema resuelve el principio de responsabilidad única en este procesador de pagos?

Separar responsabilidades evita que una clase haga de todo: validar, cobrar, notificar y loguear. El punto de partida fue un procesador rudimentario que incumplía SOLID. Se reorganizó con clases específicas: CustomerValidation, PaymentDataValidator, Notifier, TransactionLogger y un StripePaymentProcessor, coordinadas por PaymentService.

  • Importaciones y entorno al inicio del archivo: organización estándar de la industria.
  • PaymentProcessor como data class: instanciación simple sin definir init manual.
  • processTransaction con type hints: retorna una instancia de Stripe Charge.
  • Validaciones con excepciones: ValueError en lugar de objetos vacíos.
  • Corrección de flujo: si no hay email ni teléfono, igual se devuelve el charge exitoso.

¿Cómo estaba el código antes y qué ajustes iniciales se hicieron?

  • Importaciones y variables de entorno (por ejemplo, stripe_api_key) movidas al inicio.
  • PaymentProcessor convertido en data class para simplificar instanciación.
  • Firma de processTransaction conservada, ahora con retorno tipado a Charge.
  • Validaciones reemplazadas por raise ValueError con mensajes claros.
  • Bug corregido: ausencia de email y teléfono ya no corta el flujo; se devuelve el cargo igualmente.

¿Cuáles responsabilidades se identifican y por qué separarlas?

  • Validación de customer: nombre y contact info correctos.
  • Validación de pago: existencia de source en payment_data.
  • Procesamiento del pago: interacción con la librería de Stripe.
  • Notificación: por email o SMS, comportamiento moqueado.
  • Registro en logs: persistir contexto del cargo.

Separarlas permite que cada clase cambie por razones únicas, cumpliendo SRP.

¿Qué clases nuevas encapsulan cada responsabilidad?

  • CustomerValidation con validate(customer_data).
  • PaymentDataValidator con validate(payment_data).
  • Notifier con sendConfirmation(customer_data) para email o teléfono.
  • TransactionLogger con log(customer_data, payment_data, charge).
  • StripePaymentProcessor (renombrado para ser semántico) con processTransaction(...).
  • PaymentService como orquestador.

Ejemplo de estructura mínima:

from dataclasses import dataclass class CustomerValidation: def validate(self, customer_data: dict) -> None: if not customer_data.get("name"): raise ValueError("missing name") if not customer_data.get("contact_info"): raise ValueError("missing contact info") class PaymentDataValidator: def validate(self, payment_data: dict) -> None: if "source" not in payment_data: raise ValueError("missing source") class Notifier: def sendConfirmation(self, customer_data: dict) -> None: if customer_data.get("email"): print("enviar email moqueado.") elif customer_data.get("phone"): print("enviar sms moqueado.") class TransactionLogger: def log(self, customer_data: dict, payment_data: dict, charge): print(f"pagó {payment_data['amount']} la persona {customer_data['name']}")

¿Cómo coordina PaymentService el flujo con validadores, processor y notificaciones?

PaymentService concentra el flujo: valida datos, procesa con StripePaymentProcessor, notifica con Notifier y registra con TransactionLogger. Devuelve el Charge de Stripe y propaga stripe_error si algo falla. Se usa try/except para manejar ValueError ya emitidos por los validadores.

@dataclass class PaymentService: customer_validator: CustomerValidation payment_validator: PaymentDataValidator stripe_payment_processor notifier: Notifier transaction_logger: TransactionLogger def processTransaction(self, customer_data: dict, payment_data: dict) -> "Charge": try: self.customer_validator.validate(customer_data) except ValueError as e: raise e try: self.payment_validator.validate(payment_data) except ValueError as e: raise e try: charge = self.stripe_payment_processor.processTransaction(customer_data, payment_data) except stripe_error as e: raise e self.notifier.sendConfirmation(customer_data) self.transaction_logger.log(customer_data, payment_data, charge) return charge

¿Qué cambio clave evita errores en las validaciones?

En lugar de if esperando booleanos, los validadores ya hacen raise ValueError. Por eso, en PaymentService se usa try/except y se re-lanza la misma excepción. Esto elimina falsos positivos de validación y simplifica el control de flujo.

try: customer_validator.validate(customer_data) except ValueError as e: raise e

¿Qué se corrige en notificaciones y retorno del cargo?

  • Si no hay email ni phone, el flujo continúa y se retorna el charge exitoso.
  • La notificación es moqueada: impresión por consola según disponibilidad de contacto.
  • El logger persiste contexto: monto (p. ej., 500 o 700) y nombre (p. ej., John Doe).

¿Cómo se maneja un fallo de Stripe con tokens de prueba?

  • Se reproducen fallos con token de riesgo como “radar block”: la tarjeta es declinada por defecto.
  • El StripePaymentProcessor levanta la excepción capturada; PaymentService la propaga.
  • En el código cliente, se puede envolver la llamada en try/except y reportar: “falló el procesamiento: {error}”.
try: charge = payment_service.processTransaction(customer_data, payment_data) except Exception as e: print(f"falló el procesamiento: {e}")

¿Qué habilidades y conceptos aplicas al refactor bajo SOLID?

Refactorizar con SRP exige precisión y criterio. Aquí se aplican prácticas de alto impacto para código de pagos en producción.

  • SRP (principio de responsabilidad única): cada clase cambia por una razón única.
  • Orquestación con PaymentService: coordina validación, cobro, notificación y logging.
  • Renombramiento semántico: StripePaymentProcessor refleja su dominio real.
  • Python data class: instanciación clara, sin __init__ explícito.
  • Python type hints: processTransaction -> Charge mejora legibilidad y contratos.
  • Manejo de excepciones: ValueError en validaciones; propagación de stripe_error.
  • Mocks funcionales: envío de email/SMS como impresión controlada.
  • Corrección de flujo: retorno del charge aunque no haya medios de contacto.
  • Depuración con debugger y breakpoints: verificación de estados intermedios.
  • Pruebas de error con tokens de Stripe: “radar block” simula tarjeta declinada.

¿Tú cómo separarías aún más las responsabilidades o qué cambios harías para reforzar SRP en este flujo de pagos? Comparte tus ideas y mejoras en los comentarios.