Patrones de diseño de Python
Los patrones de diseño son, en el desarrollo de software, para problemas comunessoluciones reutilizables. No son diseños completos que se puedan convertir directamente en código, sino para resolver problemas específicosplantillas o planos。
El valor central de los patrones de diseño
Los patrones de diseño son como planos estándar en el campo de la arquitectura, y proporcionan los siguientes beneficios para el desarrollo de software:
- Mejora la reutilización del código: evita reinventar la rueda
- Mejora la mantenibilidad del código: hace que la estructura del código sea más clara
- Promueve la colaboración en equipo: proporciona un lenguaje de programación y una forma de pensar unificados
- Mejora la calidad del código: las soluciones probadas en la práctica son más confiables
Analogía de los patrones de diseño en la vida cotidiana
Imagina que vas a construir una casa:
- Método original: diseñar desde cero cada vez, propenso a errores e ineficiente
- Uso de patrones de diseño: usar planos de construcción estándar, conocer la distribución estándar de sala, cocina y dormitorio
Los patrones de diseño son los "planos de construcción estándar" del desarrollo de software.
Tipos de patrones de diseño
Según la obra clásica«Design Patterns - Elements of Reusable Object-Oriented Software» (traducción al chino: «Patrones de diseño: elementos de software orientado a objetos reutilizables»)de la definición, hay 23 patrones de diseño clásicos en total. Estos patrones se pueden dividir en tres categorías según su enfoque: patrones creacionales (Creational Patterns), patrones estructurales (Structural Patterns) y patrones de comportamiento (Behavioral Patterns). Además, en el desarrollo a nivel empresarial, también hay una categoría de patrones de diseño a nivel de arquitectura de uso común.
| N.º | Patrón y descripción | Incluye |
|---|---|---|
| 1 | Patrones creacionales Se utilizan para resolver problemas de creación de objetos. Al encapsular la lógica de instanciación y ocultar los detalles de implementación, hacen que el sistema sea más flexible y extensible al crear objetos. |
|
| 2 | Patrones estructurales Se centran en la composición y colaboración entre clases y objetos, ayudándonos a construir estructuras de sistema flexibles, reutilizables y extensibles. |
|
| 3 | Patrones de comportamiento Se centran en la comunicación y asignación de responsabilidades entre objetos. Al encapsular algoritmos, estados o lógica de solicitudes, mejoran la flexibilidad y mantenibilidad del sistema. |
|
| 4 | Patrones de arquitectura empresarial Estos patrones se utilizan comúnmente en arquitecturas en capas de sistemas grandes, centrándose en la colaboración entre la capa de presentación y la capa de lógica de negocio. |
|
La siguiente imagen muestra las relaciones y la estructura jerárquica entre los diversos tipos de patrones de diseño:
Ventajas de los patrones de diseño
- Proporcionan un lenguaje de diseño unificado, permitiendo a los desarrolladores comunicar rápidamente la intención del diseño.
- Ofrecen soluciones maduras y reutilizables para problemas comunes.
- Reducen el acoplamiento del sistema y mejoran la mantenibilidad y extensibilidad del código.
- Reducen el diseño repetido, mejorando la eficiencia del desarrollo y la calidad del código.
- Ayudan a los nuevos miembros a comprender rápidamente la arquitectura del sistema y las ideas de diseño.
Los seis principios de los patrones de diseño
1. Principio de apertura y cierre (Open-Closed Principle)
Abierto a la extensión, cerrado a la modificación. El software debe adaptarse a los cambios mediante la extensión de nuevas funcionalidades, en lugar de modificar el código existente. El núcleo de la implementación de este principio radica en usar abstracciones (interfaces o clases base) para definir comportamientos estables.
2. Principio de sustitución de Liskov (Liskov Substitution Principle)
Dondequiera que aparezca una clase base, las subclases deberían poder reemplazarla. La herencia solo tiene sentido cuando las clases derivadas pueden sustituir completamente a la clase base. Este principio garantiza la corrección del polimorfismo y es un complemento importante del principio de apertura y cierre.
3. Principio de inversión de dependencias (Dependency Inversion Principle)
Los módulos de alto nivel no deben depender de los módulos de bajo nivel; ambos deben depender de abstracciones. Las implementaciones concretas deben depender de interfaces o clases abstractas, en lugar de depender directamente de clases concretas. Este principio hace que el sistema sea más fácil de extender y probar.
4. Principio de segregación de interfaces (Interface Segregation Principle)
Una clase no debería depender de interfaces que no necesita. Las interfaces grandes deben dividirse en interfaces más pequeñas y más específicas para reducir el acoplamiento y aumentar la flexibilidad.
5. Ley de Deméter (Law of Demeter)
También conocida como "principio del menor conocimiento": un objeto debe minimizar su interacción con otros objetos. En otras palabras, una clase solo debería conocer los objetos directamente relacionados con ella, reduciendo así la complejidad del sistema.
6. Principio de reutilización compuesta (Composite Reuse Principle)
Se prefiere la composición o agregación para lograr la reutilización en lugar de la herencia. La herencia causa un fuerte acoplamiento, mientras que la composición permite reemplazar dependencias de manera flexible en tiempo de ejecución, logrando un diseño estructural más elegante.
Ejemplos de patrones de diseño comunes en Python
Entendamos la aplicación de los patrones de diseño en Python a través de algunos ejemplos concretos.
Patrón Singleton
El patrón Singleton asegura que una clase tenga una única instancia y proporciona un punto de acceso global.
Ejemplo
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
print("Crear una nueva conexión de base de datos")
return cls._instance
def connect(self):
print("Conectar a la base de datos")
# Ejemplo de uso
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(f"¿Son db1 y db2 la misma instancia? {db1 is db2}") # Salida: True
Escenarios de aplicación:
- Pool de conexiones de base de datos
- Administrador de configuración
- Registrador de logs
Patrón Factory
El patrón de fábrica proporciona una interfaz para crear objetos, permitiendo que las subclases decidan qué clase instanciar.
Ejemplo
# Producto abstracto
class Notification(ABC):
@abstractmethod
def send(self, message: str):
pass
# Producto concreto
class EmailNotification(Notification):
def send(self, message: str):
print(f"Enviar correo: {message}")
class SMSNotification(Notification):
def send(self, message: str):
print(f"Enviar SMS: {message}")
# Clase de fábrica
class NotificationFactory:
@staticmethod
def create_notification(notification_type: str) -> Notification:
if notification_type == "email":
return EmailNotification()
elif notification_type == "sms":
return SMSNotification()
else:
raise ValueError("Tipo de notificación no compatible")
# Ejemplo de uso
email = NotificationFactory.create_notification("email")
sms = NotificationFactory.create_notification("sms")
email.send("Su pedido ha sido enviado")
sms.send("Código de verificación: 123456")
Explicación del código:
NotificationEs una clase base abstracta que define la interfazEmailNotificationySMSNotificationEs una implementación concretaNotificationFactorySe encarga de crear objetos de notificación concretos
Patrón Observer
El patrón de observador define una relación de dependencia de uno a muchos entre objetos; cuando un objeto cambia de estado, todos los objetos que dependen de él reciben una notificación.
Ejemplo
# Interfaz de observador
class Observer(ABC):
@abstractmethod
def update(self, message: str):
pass
# Observador concreto
class EmailSubscriber(Observer):
def __init__(self, name: str):
self.name = name
def update(self, message: str):
print(f"{self.name} recibió notificación por correo: {message}")
class SMSSubscriber(Observer):
def __init__(self, name: str):
self.name = name
def update(self, message: str):
print(f"{self.name} recibió notificación por SMS: {message}")
# Sujeto (observado)
class NewsPublisher:
def __init__(self):
self._subscribers = []
def subscribe(self, subscriber: Observer):
self._subscribers.append(subscriber)
def unsubscribe(self, subscriber: Observer):
self._subscribers.remove(subscriber)
def notify_subscribers(self, message: str):
for subscriber in self._subscribers:
subscriber.update(message)
# Ejemplo de uso
publisher = NewsPublisher()
# Crear suscriptores
alice = EmailSubscriber("Alice")
bob = SMSSubscriber("Bob")
# Suscribirse a noticias
publisher.subscribe(alice)
publisher.subscribe(bob)
# Publicar noticias
publisher.notify_subscribers("¡Python 3.12 ha sido lanzado!")
# Cancelar suscripción
publisher.unsubscribe(alice)
publisher.notify_subscribers("Esta es una notificación que solo Bob puede ver")
Consideraciones especiales de los patrones de diseño en Python
Las características dinámicas de Python
Python, al ser un lenguaje dinámico, hace que algunos patrones de diseño sean más simples de implementar:
Ejemplo
def create_payment(method):
payment_methods = {
'credit_card': CreditCardPayment,
'paypal': PayPalPayment,
'alipay': AlipayPayment
}
return payment_methods[method]()
# Uso directo, sin necesidad de una jerarquía de clases compleja
payment = create_payment('alipay')
Soporte integrado para el patrón Decorator
Python tiene sintaxis de decoradores integrada, lo que hace que la implementación del patrón decorador sea más elegante:
Ejemplo
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} tiempo de ejecución: {end - start:.2f} segundos")
return result
return wrapper
@log_execution_time
def process_data(data):
# Simular procesamiento de datos
import time
time.sleep(1)
return f"Datos procesados: {data}"
# Uso
result = process_data("Datos de ejemplo")