Aplicar el principio de inversión de dependencias en un servicio de pagos permite cambiar procesadores como Stripe u opciones offline sin tocar la lógica de alto nivel. Aquí verás cómo componer el Payment Service con protocolos, eliminar default factories e introducir inyección de dependencias para manejar cobros, recurrencias y reembolsos de forma limpia.
¿Qué asegura el principio de inversión de dependencias en el servicio de pagos?
El servicio (clase de alto nivel) no depende de detalles de clases de bajo nivel, sino de interfaces; en Python, de protocolos. Los detalles viven en los procesadores de pago que hablan con distintas pasarelas. Si se cambia el protocolo, se ajustan las implementaciones, pero el servicio se mantiene estable.
Clase de alto nivel: Payment Service. Depende de protocolos, no de implementaciones concretas.
Clases de bajo nivel: procesadores de pago, notificador, validadores, logger.
Protocolos en Python: definen el contrato. La implementación puede variar sin afectar al servicio.
Recurrencia y reembolsos: pueden usar la misma instancia del procesador (por ejemplo, Stripe) si cumple los protocolos.
Beneficio clave: menor acoplamiento y mayor facilidad de reemplazo.
¿Cómo instanciar dependencias sin default factories?
Para cumplir a fondo el principio, se eliminan los default factories del servicio: la clase de alto nivel no debe saber cómo instanciar clases de bajo nivel. En su lugar, se reciben como dependencias ya construidas.
# Dependencias de bajo nivelstripe_processor = StripePaymentProcessor()email_notifier = EmailNotifier()customer_validator = CustomerValidator()payment_validator = PaymentValidator()logger = TransactionLogger()# Clase de alto nivel: composición explícitapayment_service = PaymentService( payment_processor=stripe_processor, notifier=email_notifier, customer_validator=customer_validator, payment_validator=payment_validator, logger=logger, recurrence_processor=stripe_processor,# reutiliza Stripe para recurrencia refund_processor=stripe_processor # reutiliza Stripe para reembolsos)
default factories: se remueven del servicio para evitar acoplar construcción con lógica.
Instanciación limpia: cada dependencia se crea fuera y se inyecta.
Reutilización: Stripe cumple protocolos de recurrencia y reembolso, por eso se usa la misma instancia.
¿Cómo cambiar implementaciones con inyección de dependencias?
La inyección de dependencias permite intercambiar implementaciones sin modificar el servicio. Esto abre la puerta a usar un contenedor de inyección de dependencias en escenarios reales, aunque aquí se simula con composición manual.
# Nuevas implementaciones de bajo niveloffline_processor = OfflineProcessor()sms_notifier = SMSNotifier(gateway="custom_gateway")# Segundo servicio con otras dependencias y sin recurrencia/reembolsossecond_service = PaymentService( payment_processor=offline_processor, notifier=sms_notifier, customer_validator=customer_validator, payment_validator=payment_validator, logger=logger
)
Intercambiabilidad: cambia Stripe por offline processor sin tocar la lógica de alto nivel.
Notificadores: alterna entre Email Notifier y SMS notifier según necesidad.
Configuración: elimina recurrencia y reembolsos cuando no aplican.
¿Qué habilidades y conceptos practicas?
Diferenciar clase de alto nivel vs clases de bajo nivel.
Programar contra interfaces/protocolos para reducir acoplamiento.
Eliminar default factories en el servicio para respetar el principio.
Aplicar inyección de dependencias para componer servicios flexibles.
Reutilizar instancias cuando cumplen múltiples protocolos: menos complejidad.
Preparar el terreno para un contenedor de inyección de dependencias más adelante.
¿De qué forma aplicarías el principio de inversión de dependencias en tu propio flujo de pagos o notificaciones? Comparte tu enfoque y, si te animas, integra los casos de cobro, recurrencia y reembolso practicando esta misma composición de dependencias.
from abc importABC, abstractmethod
classHeroe(ABC): # Abstracción @abstractmethod
def atacar(self): pass
classBatman(Heroe): def atacar(self):print("Batman lanza un batarang")classSuperman(Heroe): def atacar(self):print("Superman lanza su visión láser")classSuperHeroe: def __init__(self,heroe:Heroe): # Dependencia en una abstracción
self.heroe= heroe
def realizar_ataque(self): self.heroe.atacar()# Usobatman =Batman()superman =Superman()# Ahora puedes usar cualquier héroe que implemente la interfaz `Heroe`super_heroe1 =SuperHeroe(batman)super_heroe2 =SuperHeroe(superman)
Mi solución al reto: (creo)
1 Cortar y pegar: en la clase StripePaymentProcessor están las funciones que precisamente hacen devoluciones y recurrencia, por lo que solo es cortar y pegar ese código en las respectivas clases.
2 Borrar las funciones e instanciar las clases: Luego, borras las funciones que ya no necesitan en StripePaymentProcessor, (así que ya estarías cumpliendo con el principio S), e instancias las clases así:
las interfaces PaymentProcessorProtocol, RefundPaymentProtocol, RecurringPaymentProtocol ya solo son instancias del método de la clase de StripePaymentProcessor: lo que me confunde de esto último es que, la clase en cuestión (StripePaymentProcessor) ahora sería una clase de alto nivel porque define la lógica de negocio... o no sé, agradecería si alguien puede corregirme 😅
¿Hay algo que entendí mal o que no es correcto? :<
El Principio de Inversión de Dependencias (DIP) establece que las clases de alto nivel no deben depender de clases de bajo nivel, sino de abstractions, como interfaces o protocolos. Los puntos básicos son:
Dependencias hacia abstracciones: Un módulo de alto nivel no debe depender de un módulo de bajo nivel. Ambos deberían depender de abstracciones.
Inversión de dependencias: Las dependencias deben ser inyectadas en lugar de ser creadas dentro de las clases, promoviendo un código más flexible y fácil de modificar.
Facilitación de pruebas: Permite que las pruebas unitarias sean más simples, ya que se pueden inyectar dependencias simuladas.
Este principio mejora la mantenibilidad y escalabilidad del código.
Que buena clase 5 stars!
En esta parte, tengo entendido que la inteccion se puede hacer por medio del constructor o en el metodo no?
No sé si esto está relacionado con la composición (hablando de UML) porqué, por ejemplo, PaymentService se compone de un custom_validator que debe tener sí o sí, no? por lo tanto, podría creare una nueva instancia del mismo en el constructor, en este caso en el método post_init porque es una dataclass, es correcta mi apreciacion?
En el caso de payment_validator sería lo mismo o no?
sí, al final la inversión de dependencias está muy ligado a la composición, de hecho, es lo que permite que la composición ocurra.
Lo ideal es que la clase principal no tenga la responsabilidad de crear a las clases de las que depende, para eso se utilizar diferentes mecanismos como por ejemplo el Patrón de Diseño Builder que se ve más adelante en el curso.
En resumen, de esta manera se aplica el dependency_inversion en las piezas de codigo que son mostradas en clase:
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pydantic import BaseModel
import stripe
from stripe import StripeError
import uuid
# Type hinting for the payment responseclassPaymentResponse(BaseModel): status:str amount:float transaction_id:str message:str# AbstractionclassPaymentProcessor(ABC):@abstractmethoddefprocess_transaction(self, customer_data, payment_data)-> PaymentResponse:...# Payment Type 1@dataclassclassStripePaymentProcessor(PaymentProcessor):defprocess_transaction(self, customer_data, payment_data)-> PaymentResponse: stripe.api_key = os.getenv("STRIPE_API_KEY")# Payment processing responsibilitytry: charge = stripe.Charge.create( amount=payment_data["amount"], currency="usd", source=payment_data["source"], description="Charge for "+ customer_data["name"],)print("Payment successful")return PaymentResponse( status=charge["status"], amount=charge["amount"], transaction_id=charge["id"], message="Payment successful",)except StripeError as e:print("Payment failed:", e)return PaymentResponse( status="failed", amount=payment_data["amount"], transaction_id=None, message=str(e),)# Payment Type 2@dataclassclassOfflinePaymentProcessor(PaymentProcessor):defprocess_transaction(self, customer_data, payment_data)-> PaymentResponse:print("Processing offline payment for", customer_data["name"])return PaymentResponse( status="success", amount=payment_data["amount"], transaction_id=str(uuid.uuid4()), message="Offline payment successful")# Here is where the magic happens:# In the below class 'PaymentService', the Dependency 'payment_processor' is an Abstraction of PaymentProcessor, which means once PaymentService is instantiated it can have as any Payment Type as argument, but, it follows PaymentProcessor contract.@dataclassclassPaymentService:# The high-level class depends on the PaymentProcessor abstraction instead of concrete implementations.# This allows swapping StripePaymentProcessor, OfflinePaymentProcessor, etc. without modifying PaymentService,# preserving OCP and avoiding tight coupling — otherwise changing payment logic would force modifying this class,# breaking both OCP and DIP. payment_processor: PaymentProcessor
defprocess_transaction(self, customer_data, payment_data)-> PaymentResponse: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.payment_processor.process_transaction(customer_data, payment_data) self.notifier.send_confirmation(customer_data) self.logger.log(customer_data, payment_data, charge)return charge
except StripeError as e:raise e
# Usage, first we instantiate the Payment Type 1 and 2stripe_payment_processor = StripePaymentProcessor()offline_payment_processor = OfflinePaymentProcessor()# Any implementation of PaymentProcessor can be swapped here without modifying PaymentService.stripe = PaymentService(payment_processor=stripe_payment_processor)offline = PaymentService(payment_processor=offline_payment_processor)
Asi lo hice, me gusraia rebir feedback:
classPaymentProcessorProtocol(Protocol):"""
Protocol for processing payments, refunds, and recurring payments.
This protocol defines the interface for payment processors. Implementations
should provide methods for processing payments, refunds, and setting up recurring payments.
"""defprocess_transaction( self, customer_data: CustomerData, payment_data: PaymentData
)-> PaymentResponse:...classRefundPaymentProtocol(Protocol):defrefund_payment(self, transaction_id:str)-> PaymentResponse:...classRecurringPaymentProtocol(Protocol):defsetup_recurring_payment( self, customer_data: CustomerData, payment_data: PaymentData
)-> PaymentResponse:...classStripePaymentProcessor(#PaymentProcessorProtocol, RefundPaymentProtocol, RecurringPaymentProtocol): PaymentProcessorProtocol = PaymentProcessorProtocol() RefundPaymentProtocol = RefundPaymentProtocol() RecurringPaymentProtocol = RecurringPaymentProtocol()
La finalidad de usar from abc import ABC en Python es para definir clases abstractas. Al heredar de ABC, puedes crear métodos abstractos en la clase padre que deben ser implementados en las clases hijas. Esto garantiza que las subclases sigan un contrato específico, asegurando que ciertos métodos existan y tengan la implementación adecuada, incluso si Python permite que las clases se instancien sin estos métodos. Esto mejora la organización y la mantenibilidad del código, alineándose con los principios SOLID, especialmente la responsabilidad única y la inversión de dependencias.
Otro ejemplo sencillo de un codigo que viola el DIP:
classBatman: def atacar(self):print("Batman lanza un batarang")classSuperHeroe: def __init__(self): self.heroe=Batman() # Dependencia directa de una clase concreta
def realizar_ataque(self): self.heroe.atacar()# Usoheroe =SuperHeroe()heroe.realizar_ataque()
En el código de ejemplo, las dependencias se inyectaron en la creación de la clase PaymentService. Las instancias de las dependencias como StripePaymentProcessor, EmailNotifier, CustomerValidator, PaymentValidator y Logger se pasaron como argumentos al constructor de PaymentService, garantizando que esta clase no dependa de la creación de las clases de bajo nivel.