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(...).
¿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.
@dataclassclassPaymentService: customer_validator: CustomerValidation
payment_validator: PaymentDataValidator
stripe_payment_processor
notifier: Notifier
transaction_logger: TransactionLogger
defprocessTransaction(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.
Cómo aplicar SRP en un procesador de pagos con Stripe
todos habeis olido el codeSmell del minuto 19:00?
exacto, está repitiendo
exept ValueError as e
los try/except a efectos de computo son más costosos que los if, lo ideal en este caso es tener todos los validadores dentro del try y retornar un solo except con el ValueError, porque nos da igual que ValueError sea, el except los captará y retornará independientemente del que sea. no sé si lo corregirá en videos futuros pero, seguro que más de uno se dió cuenta! ese principio no es de solid, pero se llama DRY (dont repeat yourself). excelente video profesor, no hate, nadie es perfecto, solo un pequeño apunte para ayudar a la comunidad :)
Yo creo que esta bien lo que dice el comentario porque funcionaria como un pasamanos porque el raise de las clases que hacen validate suben hasta el unico exept entonces si sabriamos de que parte es el error
Entonces esta parte podria ser asi:
Ahora mi duda es que opinan de esto:
Un dataclass en Python es una clase que se utiliza principalmente para almacenar datos. Te permite definir clases de manera más sencilla y con menos código mediante la automatización de la creación de métodos como __init__, __repr__, y __eq__.
Crear clases separadas para validar los datos del cliente y del pago refuerza el principio de responsabilidad única (SRP) al encapsular la lógica específica de cada responsabilidad en su propia clase. Aunque métodos independientes podrían parecer suficientes, agrupar funciones relacionadas en clases mejora la cohesión y la mantenibilidad del código. Esto permite una mejor gestión de los cambios, ya que cada clase se ocupa de su propia lógica, facilitando pruebas unitarias y futuras extensiones del sistema sin afectar otras partes del código.
Me tardé 2 horas en esta clase pero lo entendí todo! Se que para quienes vamos iniciando puede parecer abrumador, pero dense el tiempo de leer con calma, preguntar a la IA como guia, no la respuesta, buscar tutoriales y entender que hace cada cosa paso por paso!
si tienen la extensión de GIT en VS-Code, y quieren comparar rápidamente los archivos vistos en clase
Before.py Vs initial_code.py, Sólo tienen que copia y pegar el contenido de un archivo sobre el otro y GIT les va a marcar en:
- VERDE : Cambios (+)
- ROJO : Cambios (-)
considero que un try except se deberia de utilizar cuando uno no tiene el control de las cosas y quiere ponerle control y queres ejecutar algo en funcion del error, en este caso, solo es una validación y es raro hacer un raise del mismo error que ya viene del Validator, un ejemplo mas funcional es como cuando usas un paquete externo (en este caso stripe) y queres regresar un custom error, quitando lo que el propio paquete ya retorna (evitando exponer datos) o en transacciones de DB para hcer un rollback de varias inserciones en varias tables si alguna da error
No sé que tan de acuerdo estén con esto. Al menos esta clase me deja esto:
- Importante diferenciar las responsabilidades ya que en base a esto podemos identificar las clases necesarias. A mi parecer, esto es experiencia y/o mucha práctica.- Cuando tenemos un código que tiene una interacción con alguna otra librería y/o aplicación, debemos seperar este código a una clase que nos sirva como "puente" de comunicación con la lógica y la librería. Esto con la finalidad de que, si la librería cambia solo debamos modificar en un lugar. A estas clases, generalmente, las nombramos como _services_.- Una dataclass permite usar POO de una manera más sencilla y así poder trabajar mejor así como diferenciar responsabilidades y poder aplicar SOLID
al inicio del curso cuando el profe dijo que ibamos a crear una pasarela de pagos , me emocioné bastante , pero cuando ví que ya literalmente todo esta creado y que solo es copiar y pegar , se me quitaron las ganas de seguir viendo este curso
no pierdas el enfoque del curso, el curso no es "hacer una pasarela de pago con python" el curso es patrones de dise;o y su enfoque es saber que no es buena practica y como arreglarlo, por eso sientes que es solo "copiar y pegar", porque sino verias como lo hace mal y luego como lo arregla, se volveria largo, innecesariamente, espero lo hayas seguido viendo y sino pues te míratelo que vale mucho la pena saber esto
Mi pregunta es un poco más enfocada de cara a saber hasta dónde es bueno separar esas funciones, o cómo determinar cuál es ese límite inferior para decir: 'Estas estarían de manera independiente' o 'Es necesario dejarlas juntas'. ¿Hay alguna guía de cara a eso? ¿O algún estándar o alguna norma?
Hola, una duda, que propósito tiene implementar un bloque try except, si en el except solamente se levanta la excepción con el 'raise e' ¿el funcionamiento no sería exactamente el mismo a que si no se tuviera el try except? ya que en ambos casos se levanta la excepción sin agregar ninguna otra funcionalidad.
En este caso estas en lo correcto. Solo se usa para "notificar" en la consola que hubo un error. En un ambiente de produccion pudo haber sido loggear en algo mas visual o realizar una y otra accion pertinente. Pero mayormente, dejarle saber al usuario cual fue el error.
Hay personas que lo utlizan para que si fallo por un lado, intente otra accion (no lo recomiendo). Por ejemplo, fallo con la tarjeta principal? intenta con la secundaria... Pero hay mejores formas de hacer esto.
no hay ningún problema con que sea una clase, también puedes utilizar funciones para las validaciones. Se podría declarar que los validadores son Callable y de igual forma se pueden definir dentro de la clase principal. Dejan de ser clases y convertirse en funciones pero cumplen el mismo propósito
Al revisar el código refactorizado se ve que todas las nuevas clases solo tienen un método declarado, ya que así cumplen con el principio de responsabilidad única y queda modularizado el código como se buscaba.
La elección entre Pydantic y dataclasses depende del contexto de uso.
Pydantic es excelente para la validación de datos y la manipulación de configuraciones, ya que proporciona características como la validación automática y el soporte para tipos complejos. Es ideal para aplicaciones donde la validación de datos es crítica, como en APIs.
Dataclasses son más simples y livianas, perfectas para estructuras de datos sin validación adicional. Se utilizan cuando no se necesita la funcionalidad extra de validación y sólo se requiere una representación eficiente de los datos.
Si la validación es una prioridad, Pydantic es la mejor opción.
Entonces responsabilidad unica solo es para abstraer la clase y separar la logica? porque al final, el PaymentService igual hace uso de otras clases para lograr lo que se propone. Es decir, aunque no tenga el codigo explicitamente dentro, hace uso de clases y funciones que basicamente imprimen o usan el codigo. Entonces pareceria que al final, es lo mismo o parecido y que solo sirve para ordenar, abstraer para utlizar en otros lados y separar la logica, y asi.
que diferencia hay entre un dataclass y usar pydantic?
De hecho, considero que no está bien tener un try/except evaluando englobando varias validaciones. En el momento que salte la excepción no vas a saber a ciencia cierta cuál fue explícitamente.
La forma correcta es hacer que cada validate tenga una excepción específica. Por ejemplo, el customer_validator debe retornar un CustomerValueError. Y al momento de hacer el try hacer algo como:
try: self.customer_validator.validate(customer_data)except CustomerValueError as e:raise e
```De esta forma se hace un except específico y tampoco tienes el típico try evaluando un gran bloque de código.