Aplicar el principio abierto cerrado con Python es más simple de lo que parece: abre tu código a la extensión y ciérralo a la modificación usando modelos tipados con Pydantic y clases abstractas para comportamientos como el procesador de pagos y el notificador. Así se añade una nueva pasarela o un canal de notificación sin tocar la base.
¿Cómo aplicar el principio abierto cerrado en Python con Pydantic y clases abstractas?
El objetivo es incorporar una nueva pasarela de pagos sin reescribir lógica existente. Para ello, se reorganiza el proyecto y se tipan los datos con Pydantic. Luego, se define la forma del procesador de pagos mediante una clase abstracta, y las implementaciones concretas heredan esa forma.
Nuevo requerimiento: agregar una pasarela de pagos adicional.
Carpeta dedicada al principio open close con “antes” y “después”.
Migración de estructuras a modelos Pydantic para validación y tipado.
Ejemplo de modelos con Pydantic y acceso por atributos en vez de diccionario:
from pydantic import BaseModel
from typing import Optional
classContactInfo(BaseModel): email: Optional[str] phone: Optional[str]classCustomerData(BaseModel): name:int# según el ejemplo, tipado como entero. contactInfo: ContactInfo
classPaymentData(BaseModel): amount:int source:str
¿Qué valida Pydantic y cómo cambia el acceso a datos?
Define forma y tipos: por ejemplo, amount entero y source string.
Maneja opcionales: email y phone pueden ser None o str.
Facilita validaciones: ahora se usa customerData.name en lugar de claves de diccionario.
¿Qué cambios habilitan la extensión sin modificar el servicio?
El paso clave es depender de una abstracción. Se crea la clase abstracta del procesador de pagos con from abc import ABC, abstractmethod. La implementación concreta (por ejemplo, Stripe) hereda y define la lógica. Luego, el servicio depende de la abstracción, no de la clase concreta. Con data classes y field(default_factory=...) se fijan implementaciones por defecto sin acoplar el código.
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
classPaymentProcessor(ABC):@abstractmethoddefprocessTransaction(self, paymentData: PaymentData)->None:...classStripePaymentProcessor(PaymentProcessor):defprocessTransaction(self, paymentData: PaymentData)->None:# lógica real de Stripe aquí....@dataclassclassPaymentService: paymentProcessor: PaymentProcessor = field(default_factory=StripePaymentProcessor)# notifier se define más abajo.defpay(self, paymentData: PaymentData, customerData: CustomerData)->None: self.paymentProcessor.processTransaction(paymentData) self.notifier.sendConfirmation(customerData)
Dependencia de abstracciones: el servicio ya no acopla a StripePaymentProcessor.
Uso de polimorfismo: nuevas pasarelas implementan PaymentProcessor sin tocar el servicio.
field(default_factory=StripePaymentProcessor) crea la instancia por defecto y evita dependencias rígidas.
¿Cómo configurar y probar notificaciones por email y SMS?
Se repite el patrón con un notificador: una clase abstracta define la firma de sendConfirmation y dos implementaciones (EmailNotifier y SMSNotifier) envían por distintos canales. Se refactoriza para no validar lo que la implementación ya garantiza (por ejemplo, en EmailNotifier se asume email disponible y se manejan casos vacíos con una condición simple).
¿Cómo se abstrae el notificador?
classNotifier(ABC):@abstractmethoddefsendConfirmation(self, customerData: CustomerData)->None:...classEmailNotifier(Notifier):defsendConfirmation(self, customerData: CustomerData)->None: email = customerData.contactInfo.email or""ifnot email:# evitar error cuando email es None o vacío.return# enviar correo a email....classSMSNotifier(Notifier):defsendConfirmation(self, customerData: CustomerData)->None: phone = customerData.contactInfo.phone
# enviar SMS simulado a phone....
Integración en el servicio con notificador por defecto y configuración flexible:
@dataclassclassPaymentService: paymentProcessor: PaymentProcessor = field(default_factory=StripePaymentProcessor) notifier: Notifier = field(default_factory=EmailNotifier)defpay(self, paymentData: PaymentData, customerData: CustomerData)->None: self.paymentProcessor.processTransaction(paymentData) self.notifier.sendConfirmation(customerData)# Cambiar comportamiento sin modificar la base de código:service = PaymentService(notifier=SMSNotifier())
¿Qué comportamiento se observa al ejecutar?
Al forzar SMSNotifier, no se envía SMS si falta teléfono.
Si hay teléfono, el SMS sale “enviado” al número definido.
Sin pasar notificador, usa el predeterminado: correo electrónico.
En conjunto, se aplicó el principio abierto cerrado con: modelos Pydantic tipados, clases abstractas (PaymentProcessor, Notifier), implementaciones concretas (StripePaymentProcessor, EmailNotifier, SMSNotifier), y default factory en data classes para configurar por defecto. Comparte en comentarios: ¿de qué otra forma aplicarías este principio aquí y qué mejoras propondrías al diseño?
Porque no hacen esos cambios antes de terminar el video, al menos, a mi me confunde eso cuando debieron hacerlo de antes por eso el archivo es before
Cuéntame Ray, como te ayudo a continuar tu aprendizaje.
pues solo me retrasa un poco porque de venir con una estructura me tengo q detener de más. Creo se debe explicar esos detalles más a fondo porque me tocó investigar un poco más porque no estoy acostumbrado con pydantic.
el video esta mal editado, repite en el min 5:13 una idea ya expuesta
🧠 Preguntas clave para saber si necesitas OCP
¿Necesito modificar una clase o función cada vez que agrego una nueva funcionalidad? (Si es así, aplica OCP).
¿Puedo extender la funcionalidad agregando una nueva clase en lugar de modificar una existente?
¿Hay muchos if/else o switch/case que podrían convertirse en clases separadas?
¿Cada vez que hay un cambio en las reglas de negocio, tengo que modificar el código existente?
Asignar una clase por defecto directamente puede causar problemas porque si la clase contiene atributos mutables, todas las instancias compartirán la misma referencia a esos atributos. Esto significa que un cambio en una instancia afectará a todas las demás, rompiendo la integridad de los datos. Es fundamental en aplicaciones donde múltiples instancias coexisten, ya que cada objeto debería tener su propio estado independiente. Para evitar esto, se recomienda usar un default_factory que cree nuevas instancias en lugar de compartir la misma.
Para aplicar el **Principio Abierto/Cerrado** (Open/Closed Principle, OCP), necesitamos estructurar el código de manera que se pueda **extender sin modificar** el código existente. Esto se puede hacer mediante la creación de clases abstractas o interfaces que se implementan o heredan en clases específicas, permitiendo la adición de nuevas funcionalidades sin cambiar la clase base.
A continuación, te muestro un ejemplo y cómo aplicarle el OCP.
### Ejemplo de Código que Viola el OCP
Supongamos que estamos desarrollando un sistema para calcular el costo de envío de un pedido. Inicialmente, solo tenemos el cálculo de envío estándar, pero más adelante necesitamos agregar otros tipos de envío, como envío exprés y envío internacional. Una implementación que viola el OCP podría verse así:
Cada vez que se agrega un nuevo tipo de envío, hay que modificar la clase ShippingCalculator, lo cual viola el principio OCP, ya que no está **cerrada a modificaciones**.
### Solución: Aplicando el OCP
Para cumplir con el principio OCP, podemos crear una clase base o interfaz llamada ShippingStrategy y luego extenderla para cada tipo de envío, manteniendo el código de ShippingCalculator **cerrado a modificaciones** pero **abierto a la extensión**.
from abc import ABC, abstractmethod
\# Interfaz o clase abstracta para la estrategia de envío
classShippingStrategy(ABC):  @abstractmethod  def calculate(self, order):  pass
\# Estrategia de envío estándar
classStandardShipping(ShippingStrategy):  def calculate(self, order):  return order.weight \* 5
\# Estrategia de envío exprés
classExpressShipping(ShippingStrategy):  def calculate(self, order):  return order.weight \* 10
\# Estrategia de envío internacional
classInternationalShipping(ShippingStrategy):  def calculate(self, order):  return order.weight \* 20
\# Clase principal que utiliza la estrategia de envío
classShippingCalculator:  def \_\_init\_\_(self, strategy: ShippingStrategy):  self.strategy = strategy  def calculate\_shipping(self, order):  return self.strategy.calculate(order)
### Explicación
- **Clase ShippingStrategy**: Es una clase abstracta que define el método calculate. Esta clase sirve como interfaz para las estrategias de envío.
- **Clases StandardShipping, ExpressShipping, InternationalShipping**: Implementan ShippingStrategy y proporcionan el cálculo específico de cada tipo de envío.
- **Clase ShippingCalculator**: Utiliza una instancia de ShippingStrategy para calcular el costo de envío sin importar el tipo. De esta manera, se puede extender ShippingCalculator con nuevos tipos de envío sin modificar su código.
### Uso del Código
Supongamos que tenemos un objeto order con un atributo weight. Así es como podríamos calcular el costo de envío usando diferentes estrategias:
classOrder:  def \_\_init\_\_(self, weight):  self.weight = weight
\# Crear un pedido
order = Order(weight=10)# Ejemplo de pedido de 10 kg
\# Calcular el costo de envío estándar
calculator = ShippingCalculator(StandardShipping())print(f"Envío estándar: ${calculator.calculate\_shipping(order)}")
\# Calcular el costo de envío exprés
calculator.strategy = ExpressShipping()print(f"Envío exprés: ${calculator.calculate\_shipping(order)}")
\# Calcular el costo de envío internacional
calculator.strategy = InternationalShipping()print(f"Envío internacional: ${calculator.calculate\_shipping(order)}")
### Ventajas de Aplicar el OCP
1. **Extensibilidad**: Podemos añadir nuevos tipos de envío (por ejemplo, "Envío nocturno") simplemente creando una nueva clase que herede de ShippingStrategy, sin necesidad de modificar ShippingCalculator.
2. **Mantenibilidad**: Al no tener que modificar ShippingCalculator, se reduce el riesgo de introducir errores en la lógica existente.
3. **Reusabilidad**: Cada estrategia de envío es una clase independiente, por lo que puede reutilizarse y probarse de forma aislada.
### Conclusión
Al aplicar el **Principio Abierto/Cerrado** hemos hecho que el código sea más **modular** y fácil de **extender**. Esto nos permite agregar funcionalidades nuevas sin necesidad de modificar el código existente, lo cual es esencial para mantener un sistema robusto y escalable.
Excelente ejemplo. Muchas gracias por el esfuerzo.
En el principio abierto cerrado que pasaria si al implementar otro paymentProcessor este requiere de otros atributos? Se usaria kwargs??
En sí, pydantic para qué es? para hacer clases o solo para representar estructuras de diccionarios de una mejor manera?
Pydantic es una biblioteca de Python para la validación de datos y la gestión de configuraciones mediante el uso de modelos de datos basados en anotaciones de tipo.
FastAPI utiliza Pydantic, ya que este proporciona una forma sencilla y eficiente de definir y validar datos, así aprovecha dichas capacidades para crear APIs rápidas, seguras y bien documentadas.
Estaria bueno que compartan un archivo donde compartan las configuraciones de vscode de cada curso, por ejemplo, de este curso me gusta el theme pero debo buscar manualmente cuál es.
Son varios los cambios que hizo al código utilizando Pydantic previos a empezar la clase, me gustaría que no hicieran eso antes para evitar confusiones, tuve que ubicar varios de esos cambios para hacerlos porque no los tenía, especialmente en las firmas de los métodos. No lo digo como algo demasiado malo, solo como un detalle que hace que pauses el video muchas veces y pierdas el foco de lo que estás haciendo
concuerdo contigo
No le veo sentido hacer una clase dataclass si no tiene atributos.
Respecto a las interfaces también se puede usar Protocol, un poco más flexible y nos evita la herencia.
Otro ejemplo de uso de OCP
Sin OCP: Aqui se viola el principio porque para agregar un nuevo tipo de descuento se tendria que modificar la clase DiscountCalculator
classDiscountCalculator:def__init__(self, discount_type, amount): self.discount_type = discount_type
self.amount = amount
defcalculate_discount(self):if self.discount_type =="percentage":return self.amount *0.10elif self.discount_type =="fixed":return20elif self.discount_type =="seasonal":return self.amount *0.15else:return0# Uso del calculador de descuentoscalculator = DiscountCalculator("seasonal",100)```Con OCP:
```python
from abc import ABC, abstractmethod
classDiscountStrategy(ABC):@abstractmethoddefcalculate_discount(self, amount):passclassPercentageDiscount(DiscountStrategy):defcalculate_discount(self, amount):return amount *0.10classFixedDiscount(DiscountStrategy):defcalculate_discount(self, amount):return20classDiscountCalculator:def__init__(self, strategy: DiscountStrategy, amount): self.strategy = strategy
self.amount = amount
defcalculate_discount(self):return self.strategy.calculate_discount(self.amount)# Uso del calculador de descuentoscalculator = DiscountCalculator(SeasonalDiscount(),100)
Bueno estas clases si me tomo mas comprenderlas, aqui mis conclusiones:
El OCP se viola cuando tenemos una clase en al cual tenemos una logica que tenga que modificarse cuando queremos agregar una nueva funcionalidad. El ejemplo de la clase no queda claro como se viola el principio porque la clase se llama StripePaymentProcessor, quizas debio llamarse solo PaymentProcesor ya que si queremos implementar otra pasarela tendriamos que modificar esa clase lo cual violaria el principio. Pero aqui podriamos crear otra clase como PayPalPaymentProcessor sin modificar la de StripePaymentProcessor y no estariamos violando el principio.
Considero que este curso va muy mal, puro copiar y pegar. Implementaciones sin explicaciones base. Algunas implementaciones quizás cumplan los principios pero le encuentro poco o ningún sentido. Este formato de cursos me parece terrible. Esta bien tomar un código y hacer el refactoring, pero bajo procesos de explicación adecuados.
x2
Este curso va a una velocidad de vértigo. (Y ME ENCANTA!!).
Tuve que pausar el curso para entender primero los siguientes conceptos y librerías:
API (public - Secret keys)
Dotenv - load_dovenv() - .env
ABS (Clases abstractas)
@Dataclass
Pydantic
Una clase abstracta es una clase que no puede ser instanciada directamente y sirve como base para otras clases. Es utilizada para definir una interfaz común y establecer métodos que deben ser implementados por las clases derivadas. En el contexto de Python, se crea utilizando el módulo abc y su propósito es garantizar que las subclases sigan un contrato específico. Esto es fundamental para aplicar principios como el Abierto-Cerrado, permitiendo extender funcionalidad sin modificar el código existente.
Para que sirve el la funcion field al momento de instanciar un objeto?
Puede considerarse esta implementación de Notifier como una forma del patrón Strategy?
De no ser así, cuál sería la diferencia?
Sí, se considera una implementación del patrón Strategy. El atributo notificador del Service se utiliza como una interfaz para múltiples estrategias de notificación y el método setNotifier permite cambiar la estrategia en tiempo de ejecución según el contexto.
Esta bueno pero... por que no hacer lo del field para todos ???
'''
@dataclass