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.
Principio de inversión de dependencias: servicio de pagos flexible
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.
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.
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.
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.
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)