Skip to content

DanielCaicedo97/solid-with-python-basic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🧩 Patrones de Diseño y SOLID en Python

platzi curso link

💡 Patrones de Diseño y Principios SOLID en Python para Procesadores de Pago

El mundo del desarrollo de software ha evolucionado considerablemente en las últimas décadas gracias a dos pilares fundamentales: los patrones de diseño, que llevan 30 años organizando el caos del código, y los principios SOLID, que llevan 20 años haciendo la vida de los programadores más sencilla. Juntos, permiten construir sistemas más mantenibles, escalables y flexibles. En este curso, aprenderemos a aplicar ambos conceptos mientras desarrollamos un procesador de pagos en Python, mejorando el código a lo largo del proceso para crear una solución robusta y eficiente.

❓ ¿Por qué es importante aprender patrones de diseño y principios SOLID?

Los patrones de diseño y los principios SOLID ofrecen soluciones probadas a problemas comunes en el desarrollo de software. Aunque llevan décadas en uso, siguen siendo esenciales porque:

  • 🔧 Mejoran la mantenibilidad del código.
  • 📈 Aumentan la flexibilidad y escalabilidad.
  • 🧪 Facilitan la implementación de pruebas unitarias e integración.
  • 🚀 Optimizan el rendimiento.
  • 🧠 Mejoran la experiencia de desarrollo, haciéndola más clara y ordenada.

¿Cómo aplicaremos los patrones de diseño y los principios SOLID?

A lo largo del curso, trabajaremos sobre una base de código inicial que no cumple con ninguno de los principios SOLID ni sigue patrones de diseño. Esto nos permitirá ver, de manera práctica, cómo transformar el código paso a paso. Usaremos Python para construir un sistema de procesamiento de pagos, aplicando mejoras incrementales entre cada lección, las cuales servirán como retos para que los estudiantes reflexionen y discutan en la sección de comentarios.

Contar con un entorno de desarrollo adecuado que incluya:

  • 🖊️ Un editor de código
  • 📦 Un manejador de paquetes como PIP, UV o Poetry
  • 🧪 Un entorno virtual para gestionar dependencias
  • 🌐 GIT y GitHub para el manejo del repositorio del proyecto

🧱 Principios SOLID

🎯 Principio de Responsabilidad Única en Desarrollo de Software (SRP)

El principio de responsabilidad única es uno de los pilares de la construcción de software, establecido por Robert C. Martin. Este principio indica que una clase o función debe tener solo una razón para cambiar, lo que mejora la mantenibilidad y reduce la complejidad. Implementarlo no solo facilita las pruebas unitarias, sino que también incrementa la reutilización del código y disminuye el acoplamiento, promoviendo un sistema más cohesivo y fácil de escalar.

¿Qué implica el principio de responsabilidad única?

  • ✅ Responsabilidad única: Una clase o función debe encargarse de una sola tarea, evitando que maneje múltiples aspectos del sistema.

  • 🔁 Razón para cambiar: El código debe tener una única causa para ser modificado, asegurando que los cambios sean claros, aislados y controlables.

🛠️ ¿Qué problemas soluciona este principio?

  • 🧹 Mantenimiento más sencillo: Al dividir responsabilidades, el código se vuelve más fácil de entender, modificar y extender.

  • ♻️ Reutilización del código: Las funciones con una única responsabilidad pueden utilizarse en diversos contextos sin necesidad de duplicación.

  • 🧪 Pruebas unitarias más simples: Las funciones con responsabilidades bien definidas son más fáciles de testear, reduciendo el esfuerzo en el desarrollo. ###🧭 ¿Cómo saber cuándo aplicar el principio de responsabilidad única?

  • 🔄 Múltiples razones para cambiar: Si una clase o función tiene varias razones para ser modificada, probablemente esté manejando demasiadas responsabilidades.

  • 🧱 Alta complejidad y difícil mantenimiento: Dificultades para agregar funcionalidades o corregir errores indican que no se está aplicando correctamente este principio.

  • 🔬 Pruebas complicadas: Si realizar pruebas unitarias requiere mucha configuración o dependencias, es señal de que hay responsabilidades mal distribuidas.

  • 📋 Duplicación de código: Si una misma funcionalidad (como una validación) está repetida, conviene extraerla en una única función reutilizable.

🔧 ¿Qué hacer cuando encuentras duplicación de responsabilidades?

Cuando se identifica la duplicación de código o responsabilidades mal distribuidas, el enfoque ideal es reorganizar ese código en una función específica que pueda ser reutilizada en todos los puntos necesarios. Esto no solo reduce el trabajo redundante, sino que también asegura que los cambios futuros se realicen en un solo lugar.

💳 Procesador de Pagos con Principios SOLID y Stripe

Construir un procesador de pagos es un desafío común en la industria del desarrollo de software. En este curso lo abordaremos aplicando los principios SOLID y patrones de diseño. Partiremos de un código básico, que iremos refactorizando paso a paso para ajustarlo a las buenas prácticas a medida que avanzamos.

⚙️ ¿Cómo funciona el código inicial?

El código comienza con una clase simple que incluye un único método llamado processTransaction. Este método recibe dos parámetros:

  • 🧑 customerData

  • 💰 paymentData

Dentro del método, se realizan tres validaciones básicas:

  • ✅ Verificación de que el cliente tiene nombre

  • 📞 Verificación de que tiene información de contacto

  • 💳 Comprobación de que tiene una fuente de pago válida

💼 ¿Cómo se realiza el procesamiento del pago?

El procesamiento del pago se gestiona mediante una integración con Stripe:

  • 🔐 Se utiliza la clave API de Stripe, almacenada en variables de entorno.

  • 💸 Se crea un cargo basado en la cantidad y los datos recibidos.

  • 🛡️ El método maneja errores con un bloque try-except, enviando una notificación si el pago falla.

📢 ¿Cómo se notifican los resultados?

El código incluye mecanismos para notificar al cliente:

  • ✉️ Correo electrónico

  • 📲 Mensaje de texto

  • 💡 Dado que no hay un servidor SMTP configurado, los ejemplos de envío de correos están comentados como referencia. En cuanto a los mensajes de texto, se utiliza un mock que simula una pasarela de envío SMS.

📝 ¿Cómo se registra la información de la transacción?

Al finalizar el proceso, el método registra los detalles en un archivo llamado transactions.log, incluyendo:

  • 🧑 Nombre del cliente

  • 💲 Monto cobrado

  • ✅ Estado final del pago

  • 📁 Estos registros son útiles para futuras consultas o auditorías.

🔄 ¿Qué modificaciones se pueden hacer al procesador?

El código es flexible y permite modificar la información para probar distintos escenarios:

  • 🧾 Cambiar el nombre del cliente

  • 💸 Variar la cantidad a cobrar

  • 💳 Probar diferentes métodos de pago, incluyendo tokens de prueba de Stripe

Stripe proporciona varios números de prueba y tokens que permiten simular transacciones en distintas condiciones.

🛡️ ¿Qué papel juegan las variables de entorno?

Una parte esencial del código es la configuración de variables de entorno:

  • ⚙️ Se gestionan con el módulo .env de Python

  • 🔐 Permiten cargar la clave API de Stripe desde un archivo .env, garantizando seguridad y evitando exponer la clave directamente en el código.

🧱 Principio de Responsabilidad Única (SRP)

El principio de responsabilidad única es uno de los pilares de la construcción de software, establecido por Robert C. Martin. Este principio indica que una clase o función debe tener solo una razón para cambiar, lo que mejora la mantenibilidad y reduce la complejidad.
Implementarlo no solo facilita las pruebas unitarias 🧪, sino que también incrementa la reutilización del código 🔁 y disminuye el acoplamiento 🔗, promoviendo un sistema más cohesivo y fácil de escalar 📈.


❓ ¿Qué implica el principio de responsabilidad única?

  • Responsabilidad única: Una clase o función debe encargarse de una única tarea, lo que evita que se ocupe de múltiples aspectos.
  • 🔄 Razón para cambiar: El código debe tener una única causa para ser modificado, asegurando que los cambios sean claros y controlados.

🛠️ ¿Qué problemas soluciona este principio?

  • 🧹 Mantener el código: Al dividir responsabilidades, el código se vuelve más fácil de mantener y más económico de modificar.
  • 🔄 Reutilización del código: Las funciones con una única responsabilidad pueden ser utilizadas en diferentes contextos sin necesidad de duplicar código.
  • 🧪 Pruebas unitarias más simples: Las funciones con responsabilidades bien definidas son más fáciles de probar, reduciendo la carga de trabajo en el desarrollo.

🧭 ¿Cómo saber cuándo aplicar el principio de responsabilidad única?

  • ⚠️ Múltiples razones para cambiar: Si una clase o función tiene varias razones para ser modificada, es un buen indicio de que tiene más responsabilidades de las que debería.
  • 🚧 Alta complejidad y difícil mantenimiento: Si se encuentra complicado añadir nuevas funcionalidades o corregir errores, es probable que la falta de una clara definición de responsabilidades esté afectando el código.
  • 🧪 Dificultad para realizar pruebas unitarias: Si preparar una prueba implica mucho trabajo o configurar demasiados elementos, es señal de que el principio no se está siguiendo correctamente.
  • 🧬 Duplicación de código: Si una funcionalidad, como una validación, está replicada en varios lugares, se debería extraer a una única función y reutilizarla donde sea necesario.

🧩 ¿Qué hacer cuando encuentras duplicación de responsabilidades?

Cuando se identifica la duplicación de código o responsabilidades mal distribuidas, el enfoque ideal es reorganizar ese código en una función específica que pueda ser reutilizada en todos los puntos necesarios.
Esto no solo reduce el trabajo redundante ♻️, sino que también asegura que los cambios futuros se realicen en un solo lugar 📍.

🔗 Acoplamiento (Coupling)

🧠 ¿Qué es?

El acoplamiento se refiere al grado de dependencia entre módulos o clases. Es decir, cuánto sabe o necesita una clase de otra para funcionar.

📉 Tipos

  • Acoplamiento fuerte (tight coupling): Las clases están muy interconectadas. Si cambias una, probablemente debas cambiar otra. ❌

  • Acoplamiento débil (loose coupling): Las clases están más independientes. Pueden cambiar o reutilizarse sin afectar a otras. ✅

🛠️ Ejemplo

class EmailSender:
    def send_email(self, to, subject):
        print(f"Enviando email a {to} con asunto {subject}")

class UserNotifier:
    def __init__(self):
        self.sender = EmailSender()  # Acoplamiento fuerte

    def notify(self, user):
        self.sender.send_email(user.email, "Bienvenido")

En este caso, UserNotifier depende directamente de EmailSender, lo cual dificulta pruebas y cambios.

💡 Solución: pasar EmailSender como dependencia (inyección) para lograr acoplamiento débil.

🧩 Cohesión (Cohesion)

🧠 ¿Qué es? La cohesión describe qué tan bien están relacionadas las responsabilidades internas de una clase o módulo. Una clase con alta cohesión hace una sola cosa y la hace bien. 🎯

📉 Tipos

  • Alta cohesión: todas las funciones están estrechamente relacionadas y sirven a un mismo propósito. ✅

  • Baja cohesión: la clase tiene múltiples responsabilidades mezcladas (violando SRP). ❌

🛠️ Ejemplo

class Usuario:
    def guardar_en_db(self): ...
    def enviar_email_bienvenida(self): ...
    def calcular_impuestos(self): ...  # ¿qué hace esto aquí?

Aquí hay baja cohesión: métodos que no deberían estar en la misma clase.

🧠 ¿Qué buscamos en buen diseño?

Concepto Meta ideal

  • 🔗 Acoplamiento Débil (Loose)

  • 🧩 Cohesión Alta (High)

  • ➡️ Alta cohesión + Bajo acoplamiento = Código mantenible, reutilizable y testeable.

🧱 Aplicando el Principio de Responsabilidad Única (SRP) en un Procesador de Pagos en Python

Para aplicar el principio de responsabilidad única (SRP) sobre un procesador de pagos en Python, se debe organizar el código para que cada clase o método tenga una única responsabilidad. A continuación, te explico cómo refactorizar el código original, identificando responsabilidades y dividiéndolo en componentes más manejables.


❌ ¿Cómo estaba estructurado el código original?

El código inicial contenía varias responsabilidades dentro de una sola clase llamada PaymentProcessor. Estas incluían:

  • ✅ Validación de datos del cliente
  • 💳 Validación de datos de pago
  • 🔌 Procesamiento del pago con Stripe
  • ✉️ Envío de notificaciones (email o SMS)
  • 📝 Registro de la transacción en logs

Este enfoque viola el principio SRP, ya que una sola clase está encargada de múltiples tareas, dificultando el mantenimiento y la escalabilidad del sistema.


🔧 ¿Cómo refactorizar el código aplicando SRP?

El primer paso fue identificar las distintas responsabilidades dentro de la clase. Se encontraron cuatro bloques clave:

  1. 👤 Validación de datos del cliente
  2. 💳 Validación de datos del pago
  3. 💸 Procesamiento del pago
  4. 📬 Notificación y logging de transacciones

🧩 ¿Cómo organizar las nuevas clases?

1️⃣ ¿Cómo separar la validación de datos del cliente?

Se creó una clase CustomerValidation con el método validate, encargada exclusivamente de validar datos del cliente, como nombre y contacto.
🔄 El código fue extraído desde PaymentProcessor y reubicado aquí.


2️⃣ ¿Cómo manejar la validación del pago?

De forma similar, se creó una clase PaymentDataValidator encargada de validar los datos del pago, como la fuente de pago (ej. tarjeta o token válido).


3️⃣ ¿Cómo procesar el pago sin romper SRP?

El procesamiento del pago, que involucra la interacción con la API de Stripe, se ubicó en una nueva clase: StripePaymentProcessor.
💼 Esta clase solo procesa pagos, cumpliendo con SRP.


4️⃣ ¿Cómo gestionar las notificaciones?

Se creó una clase Notifier, encargada de enviar notificaciones (email o SMS).
💬 Esto aísla esta lógica del resto del código, facilitando futuras modificaciones.


5️⃣ ¿Cómo registrar logs de transacciones?

Se añadió una clase TransactionLogger, dedicada a registrar la información de las transacciones en un archivo de log.
🗃️ Así se centraliza el manejo de logs, mejorando la organización.


🤝 ¿Cómo coordinar todas estas clases?

Se integraron todas las clases en una nueva entidad llamada PaymentService, encargada de:

  • Orquestar la validación de datos
  • Coordinar el procesamiento de pagos
  • Enviar notificaciones
  • Registrar transacciones

✅ Esto permite una coordinación clara y respetuosa del SRP.


🛡️ ¿Cómo se manejan los errores y excepciones?

Cada clase maneja sus propias excepciones:

  • 🚫 Las validaciones lanzan errores específicos si los datos son inválidos.
  • 🔁 Se agregó un bloque try-except en el flujo principal para capturar fallos en el procesamiento de pagos.
  • ⚠️ Esto permite una gestión de errores clara, controlada y localizada.

🧩 Principio Abierto-Cerrado (Open-Closed Principle) OCP

El principio abierto-cerrado (Open-Closed Principle) es clave para mantener la flexibilidad y estabilidad en el desarrollo de software, permitiendo que el código sea ampliado sin ser modificado. Este principio garantiza que el software pueda evolucionar de manera eficiente sin afectar las funcionalidades ya probadas, lo que es fundamental en un entorno de cambio constante como el de las empresas tecnológicas.


❓ ¿Qué es el principio abierto-cerrado?

El principio abierto-cerrado establece que el software debe estar:

  • 🟢 Abierto para su extensión, pero
  • 🔴 Cerrado para su modificación.

Esto significa que es posible añadir nuevas funcionalidades sin alterar el código existente, lo que ayuda a evitar errores y mantiene la estabilidad del sistema.


🛠️ ¿Cómo se aplica en el desarrollo de software?

  • 🔧 Extensión sin modificación: Se agregan nuevas características utilizando mecanismos como interfaces, abstracciones o polimorfismos. En lenguajes como Python, estas herramientas son parte del propio lenguaje y permiten ampliar comportamientos sin alterar la base de código original.

  • 🛡️ Cerrado para modificación: Protege el código validado, encapsulando las funcionalidades y asegurando que las nuevas extensiones no rompan lo que ya está en uso.


🌟 ¿Cuáles son los beneficios de aplicarlo?

  • Menos errores: Al no tocar el código existente, se minimizan los errores derivados de cambios imprevistos.
  • 🚀 Actualizaciones más rápidas: La extensión del software se vuelve más ágil, lo que es crucial cuando hay cambios constantes de requisitos en las empresas.
  • 🧱 Estabilidad del sistema: El código probado y validado permanece inalterado, lo que facilita el desarrollo de nuevas funcionalidades sin riesgos.

⏳ ¿Cuándo deberías aplicar el principio abierto-cerrado?

Este principio es útil cuando necesitas añadir nuevas funcionalidades sin afectar el código existente.
Un ejemplo común es:

  • ➕ La integración de una nueva pasarela de pagos en un sistema ya funcional
  • 📩 Agregar un método de notificación sin cambiar las implementaciones actuales

También es recomendable en contextos donde los requisitos del sistema cambian frecuentemente, como en la construcción de plataformas de pago o servicios con muchas interacciones externas.


🧠 ¿Cómo puedes aplicarlo en tu día a día?

Reflexiona sobre cómo usarías este principio al:

  • Agregar nuevas pasarelas de pago al sistema que estás desarrollando
  • Recordar momentos en los que has extendido funcionalidades sin modificar la base de código

Este enfoque te permitirá adaptarte rápidamente a las necesidades cambiantes de la empresa mientras mantienes la estabilidad del sistema.


🧩 ¿Cómo se aplica el principio abierto-cerrado en un procesador de pagos?

El principio de abierto-cerrado promueve la creación de código que permita extender funcionalidades sin modificar el comportamiento original.
A continuación, veremos cómo se puede aplicar este principio en el contexto de un procesador de pagos que debe añadir nuevas pasarelas sin cambiar el código existente.


La clave para implementar el principio abierto-cerrado en un sistema de pagos es diseñar el código de manera que pueda admitir nuevas pasarelas sin modificar la estructura existente.
Esto se logra utilizando clases abstractas o interfaces que actúan como intermediarios. Así, los procesadores de pagos específicos, como Stripe, pueden heredar de estas clases abstractas y añadir su propia lógica sin afectar el código original.


🔧 ¿Qué cambios se hicieron para implementar una nueva pasarela?

Para incorporar una nueva pasarela de pagos en este caso:

  • 📁 Se creó una nueva carpeta con ejemplos de antes y después de aplicar el principio abierto-cerrado.
  • 🔄 Los datos del cliente y el pago se refactorizaron utilizando Pydantic, que es la librería más popular en Python para validaciones de datos.
  • 📊 Se introdujeron modelos de datos tipados para CustomerData y PaymentData, con campos claros como montos (enteros) y fuentes de pago (cadenas de texto).

📥 ¿Cómo se manejan los datos con Pydantic?

Pydantic permite definir modelos de datos que facilitan la validación y tipado.
Por ejemplo, el modelo CustomerData contiene atributos como name (nombre del cliente) y contact_info (información de contacto), que incluye campos opcionales como teléfono o correo electrónico.
Esto hace que la manipulación de datos sea más clara y segura ✅.


🧱 ¿Cómo se crean nuevas pasarelas de pagos usando clases abstractas?

  • Primero, se define una clase abstracta que representará un procesador de pagos.
  • Esta clase no contiene lógica interna, sino que se utiliza para definir la firma del método principal, processTransaction.
  • Los procesadores de pagos como Stripe implementan esta clase abstracta, heredando su forma y añadiendo la lógica específica para procesar las transacciones.

🔹 Se define una clase abstracta PaymentProcessor que incluye el método processTransaction.
🔹 Stripe, por ejemplo, hereda de esta clase abstracta para implementar su propia lógica de procesamiento.
🔹 El servicio de pagos ya no depende de la implementación concreta de Stripe, sino que interactúa con la clase abstracta, lo que facilita añadir nuevas pasarelas sin tocar el código base 🧩.


📬 ¿Cómo se manejan las notificaciones de confirmación?

Se siguió una estrategia similar para las notificaciones.
Al igual que con los procesadores de pagos, se creó una clase abstracta Notifier que define la firma del método SendConfirmation.
Esto permite crear diferentes implementaciones, como un notificador por correo electrónico 📧 o un notificador por SMS 📱, sin afectar la estructura del código original.

🔸 Se introdujo un EmailNotifier y un SMSNotifier, ambos heredando de la clase abstracta Notifier.
🔸 El código decide dinámicamente qué tipo de notificación enviar según los datos del cliente, ya sea por correo o por SMS.


📈 ¿Cómo se extendió el código sin modificar su estructura original?

Al final, el servicio de pagos se extendió para que permitiera enviar notificaciones vía SMS sin cambiar su base de código.
Esto se logró creando una nueva clase SMSNotifier que también hereda de Notifier.
El código puede cambiar entre el envío de correos o mensajes de texto con solo ajustar la implementación del servicio, cumpliendo así con el principio abierto-cerrado ✅.


🔁 El principio de sustitución de Liskov (LSP)

Es clave para garantizar la coherencia y la interoperabilidad en sistemas orientados a objetos. Propone que las subclases deben ser intercambiables con sus clases base sin alterar el comportamiento esperado. Esto evita problemas inesperados y asegura que las clases que implementen una interfaz o hereden de otra puedan utilizarse de manera consistente, facilitando la reutilización del código y reduciendo errores en tiempo de ejecución.

❓ ¿Qué establece el principio de sustitución de Liskov?

Este principio, propuesto por Barbara Liskov, establece que las subclases deben ser sustituibles por sus clases base sin afectar el comportamiento del programa. Es esencial para asegurar que el sistema se mantenga coherente y funcione correctamente cuando se emplean clases derivadas.

🧩 ¿Cómo se aplican las subclases en LSP?

Las subclases deben respetar el contrato de la clase base. Esto significa que:

  • ❌ No se puede cambiar la firma de los métodos.
  • ⚠️ No se deben agregar nuevos atributos que afecten la funcionalidad de los métodos existentes.
  • 🔗 La interfaz y los tipos deben mantenerse compatibles.

🛡️ ¿Qué errores evita el principio de sustitución?

El LSP ayuda a evitar errores como:

  • ❗ Excepciones inesperadas cuando se requieren parámetros adicionales no previstos en la clase base.
  • 🚫 Cambios en el tipo de retorno de los métodos que interrumpen la compatibilidad entre clases.

✅ ¿Cuáles son los beneficios del principio de sustitución?

  • 🔄 Reutilización del código: Las clases que cumplen con LSP pueden ser utilizadas en diferentes contextos sin modificaciones.
  • 🤝 Compatibilidad de interfaces: Facilita que las clases puedan interactuar de forma coherente sin errores inesperados.
  • 🧱 Reducción de errores en tiempo de ejecución: El código se mantiene predecible y coherente, disminuyendo la posibilidad de fallos imprevistos.

📌 ¿Cuándo aplicar el principio de sustitución de Liskov?

Es necesario aplicarlo cuando:

  • ⚠️ Hay violación de precondiciones o poscondiciones, es decir, cuando los parámetros o el tipo de retorno de los métodos cambian.
  • 🛑 Se presentan excepciones inesperadas al usar subclases, lo que indica que no se puede hacer una sustitución sencilla entre ellas.

🔄 El principio de sustitución de Liskov En el procesador de pagos

nos permite escribir código flexible y reutilizable, pero también requiere que todas las clases o protocolos cumplan con una firma coherente. En esta clase, hemos aplicado este principio en Python reemplazando las clases abstractas con protocolos y detectando un bug deliberado que rompe este principio. Vamos a analizar cómo lo resolvimos y cómo aseguramos que las clases de notificación sean intercambiables sin modificar el código base.

🧬 ¿Cómo reemplazamos las clases abstractas por protocolos?

  • 🔁 Se sustituyeron las clases abstractas por protocolos en Python.
  • 🧩 Los protocolos actúan de manera similar a las interfaces en otros lenguajes de programación.
  • 📤 En este caso, el Notifier y el PaymentProcessor fueron convertidos en protocolos.
  • 📚 Los métodos dentro de los protocolos fueron documentados usando docstrings en formato NumPy para mejorar la claridad.

🐞 ¿Cómo se introdujo y detectó el bug?

  • 💣 Se introdujo un bug a propósito al cambiar la clase SMSNotifier.
  • ❌ El bug hizo que el método SendConfirmation no cumpliera con la firma requerida, ya que estaba aceptando un parámetro adicional: SMSGateway.
  • 🔄 Esto provocaba que no fuera intercambiable con la clase EmailNotifier, lo que viola el principio de sustitución de Liskov.
  • 🕵️‍♂️ Para detectarlo, se utilizó el debugger y un análisis de la firma del método.

⚠️ ¿Qué desafíos presenta el principio de sustitución de Liskov?

  • 🧷 Mantener la consistencia en las firmas de los métodos entre clases hijas y protocolos es crucial.
  • 🚫 Es fundamental evitar introducir parámetros adicionales o cambiar las firmas de los métodos, ya que esto rompe la intercambiabilidad de las clases.

🧩 El principio de segregación de interfaces (ISP)

Es clave en la construcción de software flexible y modular. Su enfoque evita que las clases dependan de interfaces que no necesitan, promoviendo la cohesión y disminuyendo el acoplamiento. Este principio es esencial cuando trabajamos con dispositivos multifuncionales, como impresoras y escáneres, ya que cada dispositivo solo debe implementar lo que realmente usa.

🧠 ¿Qué es el ISP? El Principio de Segregación de Interfaces dice:

"Una clase no debe ser forzada a depender de métodos que no utiliza."

Dicho más fácil: si tienes una interfaz (o clase base) con muchos métodos, y hay clases hijas que solo usan algunos, ¡divídela!. Las clases deben implementar solo lo que realmente necesitan.

❓ ¿Qué establece el principio de segregación de interfaces?

Este principio dice que los clientes no deben depender de interfaces que no utilizan. En el caso de una impresora multifuncional, por ejemplo, esta debería implementar interfaces para imprimir y escanear por separado. Si solo imprime, no necesita la capacidad de escaneo, y viceversa.

✅ ¿Cuáles son las ventajas de aplicar este principio?

  • 🎯 Mejora la cohesión y reduce el acoplamiento: Al separar los comportamientos, las clases son más especializadas y enfocadas en una tarea concreta.
  • 🔁 Reutilización de componentes: Las interfaces segregadas permiten reutilizar partes del código sin tener que implementar todos los comportamientos en una misma clase.
  • 🧱 Aislamiento de cambios: Si una interfaz, como la de impresión, cambia, las demás (como la de escaneo) no se ven afectadas.
  • 🧪 Facilidad para realizar pruebas unitarias: Al tener interfaces pequeñas y específicas, es más sencillo probar cada comportamiento de manera aislada.

🕒 ¿Cuándo debemos aplicar el principio de segregación de interfaces?

  • 📏 Interfaces con demasiados métodos irrelevantes: Si una interfaz contiene muchos métodos que no son necesarios para todas las clases, es el momento de dividirla.
  • 🚫 Clases que no usan todos los métodos: Cuando una clase no necesita todos los métodos de una interfaz, esto indica que es necesario implementar el ISP.
  • 🧨 Cambios que afectan a muchas clases: Si al modificar una interfaz varias clases se ven afectadas, es un claro signo de que el principio de segregación es necesario.

💡 ¿Cómo podrías aplicar el principio de segregación de interfaces a tu código?

Este principio es útil en sistemas donde ciertos módulos o clases tienen funcionalidades diversas. Para el procesador de pagos, ¿cómo segmentarías las interfaces para que cada clase implemente solo lo que necesita? Por ejemplo, podrías separar el procesamiento de tarjetas y la gestión de transacciones en interfaces diferentes, garantizando que los cambios en una parte del sistema no afecten otras funcionalidades.

⚙️ Implementar el principio de segregación de interfaces

es clave para mantener el código limpio y flexible en sistemas complejos como un procesador de pagos. En este caso, se abordaron varias mejoras dentro del procesador de pagos que incluyen la creación de métodos específicos para reembolsos 💸 y recurrencias 🔁, junto con la correcta segregación de las interfaces según las capacidades de cada procesador de pago.


🔄 ¿Qué cambios se realizaron en el procesador de pagos?

✅ Se agregaron dos nuevos métodos: reembolso y creación de recurrencias.
✅ Se implementó un segundo procesador de pagos, el procesador offline, que simula pagos en efectivo 💵. Sin embargo, este procesador no puede realizar reembolsos ni crear recurrencias.
✅ El método processTransaction fue modificado para no depender del atributo de Stripe, ya que ahora hay múltiples procesadores.


🚫 ¿Por qué falló la implementación del principio de segregación de interfaces?

El procesador de pagos offline implementaba métodos que no podía usar, como los reembolsos y las recurrencias, lo que violaba el principio de segregación de interfaces.
Este principio establece que una clase no debe depender de métodos que no puede implementar o usar ❌.


🛠️ ¿Cómo se corrigió el problema?

🔧 Se crearon dos nuevos protocolos:

  • RefundPaymentProtocol para reembolsos 💸
  • RecurringPaymentProtocol para recurrencias 🔁

🧩 Estos protocolos definen exclusivamente los métodos para esas acciones, eliminando la necesidad de que procesadores como el offline implementen métodos que no necesitan.

💼 Los procesadores que pueden realizar todas las acciones, como Stripe, ahora implementan los tres protocolos: uno para cobros, otro para reembolsos y otro para recurrencias.


🧰 ¿Qué otros ajustes se hicieron en el servicio?

  • Se agregaron atributos opcionales para los procesadores de reembolsos y de recurrencias (RefundProcessor y RecurringProcessor), permitiendo que cada tipo de procesador maneje solamente las acciones que le corresponden.
  • Se implementaron validaciones para evitar excepciones en caso de que un procesador no soporte ciertas acciones.
    🚨 Si el procesador no soporta reembolsos o recurrencias, se lanza una excepción con un mensaje claro.

🔍 ¿Cómo afecta este cambio al procesador de Stripe y al procesador offline?

  • Stripe ahora maneja los tres protocolos, permitiendo cobrar, hacer reembolsos y gestionar pagos recurrentes.
  • ✅ El procesador offline, al no manejar reembolsos ni recurrencias, ya no tiene que implementar esos métodos, cumpliendo con el principio de segregación de interfaces 🎯.

🔄 El principio de inversión de dependencias (Dependency Inversion Principle)

Es uno de los pilares en la construcción de software robusto, ya que busca disminuir la dependencia entre módulos de alto y bajo nivel, mejorando la flexibilidad y testabilidad del código. Este principio establece que tanto los módulos de alto como de bajo nivel deben depender de abstracciones, no de implementaciones concretas.

¿En qué consiste el principio de inversión de dependencias?

La definición formal indica que los módulos de alto nivel, que contienen la lógica de negocio, no deben depender de los módulos de bajo nivel, que gestionan los detalles de implementación. Ambos deben depender de abstracciones. Esto garantiza que los detalles de implementación dependan de las abstracciones y no al revés. Así, se facilita el cambio de implementaciones sin afectar al sistema principal.

⚙️ ¿Cómo se aplica este principio en un sistema real?

Un ejemplo claro es un gestor de notificaciones con una interfaz que define el método enviar mensaje. Este gestor es un módulo de alto nivel que no depende de cómo se implementa el envío de mensajes. Las clases de bajo nivel, como el notificador por email 📧 o por SMS 📱, implementan esa interfaz, y el gestor puede cambiar de una a otra sin modificar su código. Esto muestra cómo el principio reduce el acoplamiento y facilita el mantenimiento 🛠️.

🎯 ¿Qué beneficios trae el principio de inversión de dependencias?

  • 🔗 Modularidad: Al abstraer las implementaciones, las clases de alto nivel se mantienen independientes de los detalles.
  • 🔄 Flexibilidad: Cambiar algoritmos o implementaciones es sencillo, ya que el sistema depende de interfaces y no de clases específicas.
  • 🧪 Testabilidad: Facilita el uso de mocks en pruebas unitarias, simulando comportamientos sin depender de entornos complejos, como bases de datos.

📌 ¿Cuándo aplicar el principio de inversión de dependencias?

  • Cuando hay alto acoplamiento entre módulos de alto y bajo nivel, dificultando el mantenimiento 🔧.
  • Si se presentan problemas para cambiar implementaciones sin afectar el resto del sistema ⚠️.
  • Al realizar pruebas unitarias complicadas por la dependencia directa de implementaciones concretas 🧪.
  • Cuando se dificulta la reutilización de componentes o el escalado del sistema 📈.

PATRONES DE DISEÑO 🚀📒


📐 Los patrones de diseño

Son una herramienta poderosa en el desarrollo de software que permite resolver problemas comunes de una manera eficiente y reutilizable. En esta introducción, exploraremos qué son, por qué existen y cuáles son los principales tipos de patrones, como los creacionales, estructurales y de comportamiento, junto con ejemplos prácticos que te permitirán comprender su utilidad en la creación de software de calidad.


❓ ¿Qué son los patrones de diseño y por qué son importantes?

Los patrones de diseño son soluciones reutilizables para problemas comunes en el desarrollo de software. Imagina los patrones de diseño como recetas culinarias 🍳: cada patrón es como una receta que se aplica en condiciones específicas para resolver un problema recurrente en la programación.

Surgieron del libro "Design Patterns", escrito por cuatro autores conocidos como "La Banda de los Cuatro" 👨‍👨‍👦‍👦. Estos autores clasificaron 23 patrones en tres categorías distintas: creacionales, estructurales y de comportamiento.

Al igual que los principios SOLID, los patrones de diseño facilitan la creación de un código más mantenible y reutilizable 💾, y ayudan a establecer un lenguaje común entre los desarrolladores 🗣️.


🏗️ ¿Cuáles son los patrones de diseño creacionales?

Los patrones creacionales se centran en la creación de instancias de objetos, especialmente cuando una clase es compleja de instanciar debido a sus múltiples atributos o condiciones previas a su creación.

Hay cinco patrones de diseño creacionales destacados:

  • 🔁 Singleton: Garantiza que una clase tenga solo una instancia y proporciona un punto de acceso global a ella.
  • 🏭 Factory Method: Define una interfaz para crear un objeto, pero deja que las subclases decidan qué clase instanciar.
  • 🧱 Abstract Factory: Ofrece una interfaz para crear familias de objetos relacionados sin especificar sus clases concretas.
  • 🧑‍🍳 Builder: Separa la construcción de un objeto complejo de su representación, permitiendo crear diferentes tipos de objetos con el mismo proceso.
  • 🧬 Prototype: Permite copiar instancias existentes sin depender de sus clases.

🧩 ¿Qué son los patrones de diseño estructurales?

Los patrones de diseño estructurales están enfocados en la composición efectiva y escalable de clases y objetos. Proveen soluciones para estructurar tus clases de manera que maximicen la eficiencia y escalabilidad 🚀.

Los siete patrones estructurales clave son:

  • 🔌 Adapter: Permite que interfaces incompatibles trabajen juntas.
  • 🌉 Bridge: Desacopla una abstracción de su implementación para que ambas puedan variar independientemente.
  • 🌳 Composite: Permite componer objetos en estructuras de árbol para representar jerarquías parte-todo.
  • 🎨 Decorator: Añade nuevas funcionalidades a un objeto de manera dinámica.
  • 🏛️ Facade: Simplifica la interfaz de un conjunto de clases.
  • 🪶 Flyweight: Usa compartición para soportar de manera eficiente grandes cantidades de objetos.
  • 👥 Proxy: Proporciona un sustituto o marcador de posición para controlar el acceso a otros objetos.

🤝 ¿Cómo ayudan los patrones de diseño de comportamiento?

Los patrones de diseño de comportamiento abordan la comunicación y asignación de responsabilidades entre objetos 🧠, facilitando la interacción entre clases de distintas naturalezas.

Entre los 11 patrones de comportamiento propuestos, algunos de los más destacados son:

  • 👁️ Observer: Permite a un objeto notificar a otros objetos sobre cambios en su estado.
  • 🧠 Strategy: Define una familia de algoritmos, encapsula cada uno, y los hace intercambiables.
  • 🧾 Command: Encapsula una solicitud como un objeto, permitiendo parametrizar a los clientes con diferentes solicitudes.
  • 🔁 Iterator: Proporciona una manera de acceder secuencialmente a elementos de un objeto agregado sin exponer su representación subyacente.
  • 🗣️ Mediator: Define un objeto que encapsula cómo interactúan un conjunto de objetos.
  • 🔄 State: Permite a un objeto alterar su comportamiento cuando su estado interno cambia.
  • 🧭 Visitor: Permite definir nuevas operaciones sobre objetos de una estructura sin cambiar las clases de los elementos sobre los cuales opera.

🛠️ ¿Cómo se aplican los patrones de diseño en la industria?

En proyectos reales, no es necesario usar todos los patrones de diseño 📦. La clave está en seleccionar aquellos que mejor se adapten al contexto específico del proyecto 🎯.

En este curso, se verán algunos de los patrones más utilizados en la industria 🏭, aplicables al tipo de software que desarrollaremos, y serán clave para resolver problemas comunes de manera eficiente ✅.


🧠 Patrón Strategy

El patrón de diseño Strategy es una herramienta clave en el desarrollo de software, permitiendo cambiar dinámicamente entre diferentes algoritmos o estrategias para resolver un problema, sin alterar la estructura del programa. Este patrón es ideal para situaciones donde múltiples soluciones son viables, adaptándose al contexto en tiempo de ejecución, como lo ejemplifica el procesamiento de pagos 💳.


❓ ¿Qué es el patrón Strategy?

Este patrón de comportamiento facilita el intercambio de algoritmos que resuelven el mismo problema de distintas formas 🔄. Es útil en situaciones donde diferentes estrategias pueden ser aplicadas según el contexto, permitiendo que el programa sea flexible y adaptable sin modificar su estructura central 🏗️.


🕹️ ¿Cómo permite el patrón modificar estrategias en tiempo de ejecución?

El patrón Strategy permite la modificación de la estrategia mediante métodos que cambian la clase o el algoritmo que se está utilizando 🔧. En el ejemplo presentado, se utiliza el método SetProcessor, que permite al servicio de pagos intercambiar entre diferentes procesadores de pago durante la ejecución del programa 🔁.


💻 ¿Cómo se implementa en el código?

  • Se define una interfaz o protocolo que las diferentes estrategias deben implementar 📑.
  • La clase de alto nivel, en este caso PaymentService, no depende de las implementaciones concretas, sino de la interfaz 🧩.
  • Las estrategias concretas implementan esta interfaz, lo que permite la inyección de la estrategia adecuada según el contexto 💡.
  • Un método como SetProcessor facilita la selección y aplicación de la estrategia durante la ejecución 🔄.

🤔 ¿Cómo seleccionar la mejor estrategia?

La elección de la estrategia adecuada puede hacerse a través de una función externa o clase que analice las condiciones del problema y determine cuál es la mejor solución 🧮. Esta selección no tiene que estar dentro de la clase de alto nivel, permitiendo una mayor modularidad y escalabilidad en el sistema 📦.


✅ ¿Cuáles son los beneficios del patrón Strategy?

  • Flexibilidad para intercambiar algoritmos sin cambiar la lógica central 🔄.
  • Desacopla las clases de alto nivel de las implementaciones específicas 🔓.
  • Mejora la mantenibilidad y escalabilidad del código 🚀.

🏭 ¿Qué es el patrón creacional Factory Pattern?

El Factory Pattern es uno de los patrones de diseño más importantes en el desarrollo de software, y su principal característica es su capacidad para crear objetos sin necesidad de especificar la clase exacta 🧱. Esto se logra mediante el uso de interfaces, abstracciones o protocolos. Este patrón encapsula la lógica de creación, centralizando las decisiones sobre qué clases crear y cómo hacerlo, permitiendo así una mayor flexibilidad y escalabilidad en el sistema 🚀.


⚙️ ¿Cómo funciona el Factory Pattern?

La esencia del Factory Pattern reside en la creación de una clase llamada Factory, la cual contiene un método que instancia y devuelve objetos basados en ciertos parámetros 🧩. Al aplicar este patrón, el lugar donde se toman las decisiones de creación se centraliza, lo que facilita mantener y escalar el sistema 📈.

📌 Por ejemplo, en un diagrama de clases se podría visualizar una clase de alto nivel llamada PaymentService, que interactúa con PaymentFactory. Esta última tiene un método GetProcessor que decide cuál de los procesadores de pago usar, basándose en un tipo específico pasado como parámetro. Así, PaymentFactory centraliza la lógica de decidir cuál procesador utilizar, permitiendo fácilmente integrar nuevos procesadores en el futuro 💡.


✅ ¿Cuándo es adecuado aplicar este patrón?

El Factory Pattern se convierte en una excelente opción en los siguientes escenarios:

  • 🔁 Cuando hay múltiples clases que comparten una interfaz común. Esto es especialmente útil en sistemas donde se requiere el uso de distintas estrategias para resolver problemáticas similares.
  • 🧠 Cuando la creación de objetos requiere una compleja lógica de decisión. Si hay muchas variables para decidir qué clase seleccionar, este patrón ayuda a simplificar el proceso y establecer un control centralizado.

✨ Además, funciona muy bien junto al patrón Strategy, facilitando la selección de estrategias adecuadas para cada caso de uso.


🛠️ ¿Cómo implementarlo efectivamente?

Para implementar el Factory Pattern, sigue estos pasos:

  1. 📐 Define interfaces comunes: Asegúrate de que todas las posibles clases a instanciar implementen una interfaz o abstracción común.
  2. 🏗️ Crea la clase Factory: Desarrolla una clase que tenga un método encargado de recibir parámetros. Este método debe determinar qué clase instanciar.
  3. 🧪 Utiliza el Factory en el código cliente: Cada vez que necesites crear una instancia, utiliza el Factory para obtener el objeto correcto.

Así, al integrar el Factory Pattern, logramos crear aplicaciones más flexibles y escalables, permitiendo modificar e integrar nuevas funcionalidades sin alterar la estructura fundamental del sistema 💼.


🐍 El patrón Factory en Python se caracteriza por:

  • 🧊 Abstracción: Crea objetos sin especificar la clase exacta, utilizando interfaces o abstracciones.
  • 📦 Encapsulamiento: Centraliza la lógica de creación de objetos, simplificando la instancia de clases.
  • 🔧 Flexibilidad: Permite añadir nuevos tipos de objetos sin modificar el código existente.

📅 Cuándo aplicarlo:

  • ✅ Cuando hay múltiples clases que comparten una interfaz.
  • ✅ Cuando la creación de objetos requiere lógica compleja.

🧰 Cómo aplicarlo:

  • Crea una clase Factory con un método que instancie objetos basados en parámetros.
  • Usa el Factory donde se requieran instancias, facilitando cambios futuros 🔄.

🎨 ¿Qué es el Patrón Decorador?

El Patrón Decorador es un patrón de diseño estructural que permite añadir responsabilidades de manera dinámica a objetos. Este patrón es especialmente útil cuando necesitamos extender la funcionalidad de un objeto sin alterar su estructura original. A menudo, es necesario agregar lógica adicional sin modificar el comportamiento ya existente, lo cual es precisamente la esencia de lo que el Patrón Decorador ofrece.

✅ ¿Cuáles son las ventajas del Patrón Decorador?

Este patrón permite una composición flexible de comportamientos adicionales. Si tienes múltiples decoradores, puedes combinarlos para incrementar la lógica sobre un comportamiento base. Así, actúa como una forma modular y flexible para que los desarrolladores adapten las funcionalidades de su aplicación a diferentes situaciones sin tener que reescribir el código base.

⏱️ ¿Cuándo deberías aplicar el Patrón Decorador?

El Patrón Decorador es útil cuando necesitas combinar múltiples comportamientos de forma modular. Imagina que tienes un comportamiento deseado y, alrededor de él, necesitas otro comportamiento; en este caso, el patrón te permite componerlos. Además, es ideal para situaciones en las que se necesita añadir funcionalidades en tiempo de ejecución debido a una condición específica en el código o contexto.

🧩 ¿Cómo implementar el Patrón Decorador?

Implementar el Patrón Decorador puede parecer complejo al principio, pero se puede simplificar siguiendo estos cinco pasos:

  1. 🧱 Definir una interfaz o clase abstracta: Esta describe el comportamiento del objeto base, el cual tiene el comportamiento principal.

  2. 🛠️ Implementar la interfaz en clases concretas: Aquí, el comportamiento se abstrae hacia una interfaz y luego se implementa en varias clases concretas.

  3. 🧵 Crear una clase decoradora abstracta: Esta clase también implementa la interfaz definida en el primer paso y actúa como base para los decoradores.

  4. 🎯 Implementar decoradores concretos: Los decoradores concretos extienden la clase decoradora y contienen el comportamiento adicional que se desea implementar.

  5. 🧬 Envolver objetos básicos con decoradores: Finalmente, los objetos básicos se envuelven con los decoradores creados, permitiendo la composición de funcionalidades.

Este proceso se convierte en algo más intuitivo cuando se lo aplica en código, permitiendo visualizar claramente cómo cada componente interactúa.


🧪 Un Ejemplo de Implementación

Supongamos que tenemos un sistema para enviar notificaciones, y queremos añadir distintas formas de notificación de forma dinámica. A continuación, te mostramos una implementación básica del Patrón Decorador en Python:

# Interfaz para la notificación
class Notificacion:
    def enviar(self):
        pass

# Implementación concreta de la notificación básica
class NotificacionBasica(Notificacion):
    def enviar(self):
        return "Notificación enviada"

# Clase decoradora abstracta
class DecoradorNotificacion(Notificacion):
    def __init__(self, notificacion):
        self.notificacion = notificacion

    def enviar(self):
        return self.notificacion.enviar()

# Decorador para añadir un mensaje en el log
class NotificacionConLog(DecoradorNotificacion):
    def enviar(self):
        resultado = super().enviar()
        return f"{resultado} con registro en el log"

# Uso del patrón decorador
notificacion = NotificacionBasica()
notificacion_con_log = NotificacionConLog(notificacion)

print(notificacion_con_log.enviar())  # Salida: "Notificación enviada con registro en el log"

En este ejemplo, hemos implementado una notificación básica y posteriormente añadido un decorador para incluir un registro en el log, demostrando así cómo el Patrón Decorador puede ser utilizado para modularizar y extender funcionalidades. 💡

🧱 Builder Pattern en Python

¿Qué es el Builder Pattern? 🏗️

El Builder Pattern es un patrón de diseño creacional que se utiliza para construir objetos complejos paso a paso. Es especialmente útil cuando se desea separar el proceso de construcción de la representación final del objeto.

Una de sus principales ventajas es que proporciona una interfaz fluida para construir objetos de manera progresiva. Esto facilita la creación de diferentes variantes del mismo objeto sin modificar su estructura interna.

💡 Por ejemplo, si trabajamos con un objeto tipo Product, el Builder puede permitir construirlo parte por parte (parte A, parte B, parte C), y luego usar un método build() para obtener el objeto final.


¿Cuándo aplicar el Builder Pattern? 🕵️‍♂️

Este patrón es ideal cuando:

  • 🔁 La creación de un objeto requiere múltiples pasos.
  • 🤹‍♂️ Hay mucha lógica en el proceso de construcción.
  • 🎭 Necesitamos separar la construcción del objeto de su representación final.

Así evitamos tener constructores con demasiados parámetros o lógica embebida difícil de mantener.


¿Cómo implementarlo? ⚙️

Para usar el patrón Builder, sigue estos pasos:

  1. Crear una clase Builder que maneje el proceso de creación del objeto.
  2. Definir métodos para cada atributo que se debe configurar.
  3. Agregar un método build() que devuelva el objeto completamente armado.

Esto proporciona control y claridad, especialmente útil en objetos con múltiples configuraciones posibles.


🧪 Ejemplo en Python

# Clase Producto
class Pizza:
    def __init__(self):
        self.ingredientes = []

    def __str__(self):
        return f"Pizza con: {', '.join(self.ingredientes)}"

# Builder para Pizza
class PizzaBuilder:
    def __init__(self):
        self.pizza = Pizza()

    def agregar_queso(self):
        self.pizza.ingredientes.append("queso")
        return self

    def agregar_pepperoni(self):
        self.pizza.ingredientes.append("pepperoni")
        return self

    def agregar_champiñones(self):
        self.pizza.ingredientes.append("champiñones")
        return self

    def build(self):
        return self.pizza

# Cliente
pizza_builder = PizzaBuilder()
pizza = pizza_builder.agregar_queso().agregar_pepperoni().build()

print(pizza)  # Salida: Pizza con: queso, pepperoni

Con este patrón logramos una construcción modular, legible y escalable, ideal para objetos con muchas configuraciones opcionales 🍕

¿Qué es el Observer Pattern?

Profundizar en el Observer Pattern es descubrir un patrón de diseño que proporciona una solución eficiente para la gestión de notificaciones de eventos en un sistema. Este patrón es esencial en arquitecturas donde se necesita implementar servicios de suscripción y notificación, y por su simplicidad y eficacia, es ampliamente utilizado en la industria del software.


🧱 ¿Cómo se estructura el Observer Pattern?

El patrón se compone de tres entidades principales:

  1. 🔧 Clase Manager
    Encargada de manejar la dinámica de agregar, remover y notificar a todos los listeners suscritos.

    Métodos principales:

    • Agregar listeners: Permite suscribir objetos interesados en eventos.
    • Remover listeners: Retira aquellos que ya no necesitan notificaciones.
    • 📢 Notificar a listeners: Informa a todos los suscriptores cuando ocurre un evento.
  2. 🧩 Interfaz Listener
    Define un método update(evento) que será invocado cada vez que ocurra un evento.

  3. 📦 Clases que implementan Listener
    Son servicios o componentes que necesitan recibir notificaciones. Por ejemplo, un servicio de email o login, ambos implementando el método update.


⏰ ¿Cuándo aplicar el Observer Pattern?

Este patrón es ideal cuando:

  • 🔄 Un cambio en un objeto debe notificarse a múltiples objetos.
  • 🔔 Se necesita un sistema de eventos estructurado.
  • 🔗 Se requieren mecanismos de suscripción y notificación flexibles y desacoplados.

Su versatilidad lo convierte en una herramienta poderosa para evitar dependencias rígidas entre componentes.


🛠️ Implementación del Observer Pattern

Sigue estos 3 pasos básicos:

  1. Definir la interfaz Listener

    • Declara el método que manejará las notificaciones (update).
  2. Crear la clase Manager

    • Implementa métodos para agregar, remover y notificar a los listeners.
  3. Usar el Manager en la clase principal

    • Incluye el Manager como atributo en una clase de alto nivel, como PaymentService, que llama a notify_all() cuando se requiere comunicar un evento.

✅ Conclusión: El poder del Observer Pattern

El uso del Observer Pattern facilita la comunicación eficaz y la gestión de eventos en un sistema. Su estructura modular y flexible:

  • 🧩 Promueve la separación de responsabilidades.
  • 🔧 Mejora la mantenibilidad.
  • 📡 Permite que los componentes interactúen sin estar estrechamente acoplados.

¡Una excelente herramienta para proyectos que necesitan escalabilidad y flexibilidad desde la arquitectura! 🚀

🔗 Patrón de Diseño: Chain of Responsibility

🧠 ¿Qué es el patrón de diseño Chain of Responsibility?

El Chain of Responsibility es un patrón de diseño de comportamiento diseñado para gestionar solicitudes a través de una cadena de manejadores, cada uno con una responsabilidad única. Esta cadena permite que distintas condiciones se apliquen a una solicitud en su paso por los diferentes manejadores.

Es ampliamente utilizado en sistemas que requieren pasos de validación, como servicios de pago, autenticación o autorizaciones.


📌 ¿Cuándo deberíamos aplicarlo?

Este patrón es ideal en situaciones donde se necesita:

  • ✅ Procesar solicitudes en una serie de pasos definidos, aumentando la modularidad.
  • ✅ Implementar sistemas de validación de datos que demandan flexibilidad.
  • ✅ Crear estructuras para autenticación, autorización o validación condicional antes de ejecutar una acción.

🛠️ ¿Cómo aplicar el patrón Chain of Responsibility?

Para implementar este patrón, sigue estos pasos:

  1. Define una interfaz o clase abstracta para los manejadores
    Esta será la base para todos los componentes que participen en la cadena.

  2. Implementa cada manejador heredando de la clase base
    Cada manejador tendrá su lógica de validación específica.

  3. Configura la cadena de manejadores
    Establece el orden en que se procesan las solicitudes a través de los manejadores.

  4. Envía la solicitud al primer manejador
    Este podrá procesarla, rechazarla o delegarla al siguiente manejador.


🧪 Ejemplo de implementación (pseudocódigo)

class Validator:
    def set_next(self, handler):
        self.next_handler = handler
        return handler

    def validate(self, data):
        if self.next_handler:
            return self.next_handler.validate(data)
        return True


class MontoValidator(Validator):
    def validate(self, data):
        if data["monto"] < 1000:
            print("✅ Monto válido")
            return super().validate(data)
        print("❌ Monto no válido")
        return False


class TarjetaValidator(Validator):
    def validate(self, data):
        if data["tarjeta"] == "VISA":
            print("✅ Tarjeta válida")
            return super().validate(data)
        print("❌ Tarjeta no válida")
        return False


class FraudeValidator(Validator):
    def validate(self, data):
        if not data.get("fraud", False):
            print("✅ No se detectó fraude")
            return super().validate(data)
        print("❌ Posible fraude detectado")
        return False


# Configuramos la cadena
monto_validator = MontoValidator()
tarjeta_validator = TarjetaValidator()
fraude_validator = FraudeValidator()

monto_validator.set_next(tarjeta_validator).set_next(fraude_validator)

# Simulamos una solicitud
solicitud = {"monto": 500, "tarjeta": "VISA", "fraud": False}

if monto_validator.validate(solicitud):
    print("🎉 Solicitud aprobada")
else:
    print("🚫 Solicitud rechazada")








# 📘 Relación de Flechas y Líneas en Diagramas de Clases UML

En los **diagramas de clases UML**, las **líneas y flechas** representan diferentes tipos de relaciones entre clases. A continuación te explico los más comunes:

---

## 🔹 Línea continua con flecha abierta – **Herencia (Generalización)**

- **Símbolo:** `────────▷`
- **Significa:** Una clase hereda de otra (subclasesuperclase).
- **Ejemplo:**  
  `Perro ───────▷ Animal`  
  `Perro` hereda de `Animal`.

---

## 🔹 Línea continua sin flecha – **Asociación**

- **Símbolo:** `────────────`
- **Significa:** Una clase tiene una relación directa con otra (usa o contiene).
- **Ejemplo:**  
  `Pedido ───────── Cliente`  
  `Pedido` está asociado a `Cliente`.

---

## 🔹 Línea continua con rombo blanco – **Agregación**

- **Símbolo:** `◊────────────`
- **Significa:** Una clase contiene a otra, pero las partes pueden existir de forma independiente.
- **Ejemplo:**  
  `Departamento ◊────────── Empleado`  
  Un `Departamento` tiene `Empleados`, pero estos pueden existir sin él.

---

## 🔹 Línea continua con rombo negro – **Composición**

- **Símbolo:** `◆────────────`
- **Significa:** Una clase contiene a otra y las partes no pueden existir por separado.
- **Ejemplo:**  
  `Casa ◆────────── Habitación`  
  Las `Habitaciones` existen solo dentro de una `Casa`.

---

## 🔹 Línea de guiones con flecha abierta – **Dependencia**

- **Símbolo:** `- - - - - -▷`
- **Significa:** Una clase depende temporalmente de otra (por ejemplo, como parámetro o variable local).
- **Ejemplo:**  
  `Servicio - - - - -▷ Logger`  
  `Servicio` usa a `Logger` sin contenerlo directamente.

---

## 🔹 Línea de guiones con flecha cerrada – **Realización**

- **Símbolo:** `- - - - -▷` con triángulo vacío
- **Significa:** Una clase implementa una interfaz.
- **Ejemplo:**  
  `Usuario - - - -▷ Autenticable`  
  `Usuario` implementa la interfaz `Autenticable`.

---

>Estos símbolos te ayudan a interpretar y diseñar diagramas de clase de forma clara y profesional.

About

Aplicación de los principios SOLID y patrones de diseño en Python con un ejemplo práctico de un procesador de pagos (Stripe)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages