
Patrones de Diseño Estructurales
Aprende patrones de diseño estructurales como Adapter, Decorator, Facade y Proxy con ejemplos prácticos en Python.
¡Domina los patrones de diseño estructurales! En este tutorial especializado te guiaré paso a paso para que aprendas los patrones fundamentales de estructura, incluyendo Adapter, Decorator, Facade, Proxy, Bridge y Composite, con ejemplos prácticos y casos de uso reales en Python.
Objetivo: Aprender los patrones de diseño estructurales más importantes, sus implementaciones en Python, ventajas, desventajas y cuándo aplicarlos para organizar y simplificar la estructura de tus aplicaciones.
Índice
- Paso 1: ¿Qué son los patrones estructurales?
- Paso 2: Adapter - Conectar interfaces incompatibles
- Paso 3: Decorator - Añadir funcionalidad dinámicamente
- Paso 4: Facade - Interfaz simplificada
- Paso 5: Proxy - Control de acceso
- Paso 6: Bridge - Separar abstracción e implementación
- Paso 7: Composite - Estructuras de árbol
- Paso 8: Flyweight - Compartir objetos eficientemente
- Paso 9: Comparación y selección de patrones
- Paso 10: Proyecto práctico - Sistema de archivos
- Conclusión
- 💡 Tip Importante
Paso 1: ¿Qué son los patrones estructurales?
Los patrones de diseño estructurales se centran en cómo las clases y objetos se componen para formar estructuras más grandes. Estos patrones ayudan a asegurar que cuando una parte del sistema cambia, el resto del sistema no se vea afectado.
¿Por qué son importantes?
- Flexibilidad: Permiten cambiar la estructura sin afectar otras partes
- Reutilización: Facilitan la composición de objetos
- Mantenimiento: Simplifican la modificación de estructuras complejas
- Abstracción: Ocultan complejidad interna
Clasificación de patrones estructurales
Patrón | Propósito | Complejidad |
---|---|---|
Adapter | Conectar interfaces incompatibles | Media |
Decorator | Añadir funcionalidad dinámicamente | Baja |
Facade | Interfaz simplificada | Baja |
Proxy | Control de acceso | Media |
Bridge | Separar abstracción e implementación | Alta |
Composite | Estructuras de árbol | Media |
Flyweight | Compartir objetos eficientemente | Alta |
Paso 2: Adapter - Conectar interfaces incompatibles
El patrón Adapter permite que clases con interfaces incompatibles trabajen juntas, actuando como un puente entre diferentes interfaces sin modificar el código existente.
Implementación básica (Adapter de clase)
# Sistema existente que queremos adaptar
class ServicioWebAntiguo:
def obtener_datos_antiguos(self):
return {
"nombre_completo": "Ana García",
"edad_persona": 28,
"correo_usuario": "[email protected]",
"telefono_contacto": "+123456789"
}
# Nueva interfaz que esperamos
class ClienteDatos:
def obtener_datos_usuario(self):
# Debe devolver: {"nombre": str, "edad": int, "email": str, "telefono": str}
pass
# Adapter que convierte la interfaz antigua a la nueva
class AdapterServicioWeb(ClienteDatos):
def __init__(self, servicio_antiguo):
self.servicio_antiguo = servicio_antiguo
def obtener_datos_usuario(self):
datos_antiguos = self.servicio_antiguo.obtener_datos_antiguos()
# Transformar los datos al nuevo formato
return {
"nombre": datos_antiguos["nombre_completo"],
"edad": datos_antiguos["edad_persona"],
"email": datos_antiguos["correo_usuario"],
"telefono": datos_antiguos["telefono_contacto"]
}
# Uso del adapter
servicio_antiguo = ServicioWebAntiguo()
adapter = AdapterServicioWeb(servicio_antiguo)
cliente = ClienteDatos()
cliente = adapter # El cliente ahora puede usar el servicio antiguo a través del adapter
datos_usuario = cliente.obtener_datos_usuario()
print(datos_usuario)
# {"nombre": "Ana García", "edad": 28, "email": "[email protected]", "telefono": "+123456789"}
Adapter de objeto
from abc import ABC, abstractmethod
# Target interface
class Notificador(ABC):
@abstractmethod
def enviar_notificacion(self, mensaje, destinatario):
pass
# Adaptee (clase existente incompatible)
class ServicioEmailLegacy:
def enviar_correo(self, destinatario, asunto, cuerpo):
print(f"Enviando correo a {destinatario}")
print(f"Asunto: {asunto}")
print(f"Cuerpo: {cuerpo}")
# Adapter
class AdaptadorEmail(Notificador):
def __init__(self, servicio_email):
self.servicio_email = servicio_email
def enviar_notificacion(self, mensaje, destinatario):
# Adaptar la interfaz
self.servicio_email.enviar_correo(
destinatario=destinatario,
asunto="Notificación del Sistema",
cuerpo=mensaje
)
# Uso
servicio_email = ServicioEmailLegacy()
notificador = AdaptadorEmail(servicio_email)
# Ahora podemos usar el servicio legacy con la nueva interfaz
notificador.enviar_notificacion("Bienvenido al sistema", "[email protected]")
Caso real: Integración con APIs externas
import requests
from abc import ABC, abstractmethod
# Nuestra interfaz estándar
class ProveedorPagos(ABC):
@abstractmethod
def procesar_pago(self, monto, tarjeta, descripcion):
pass
# API externa con interfaz diferente
class ApiPagoExterna:
def charge(self, amount_in_cents, card_token, metadata):
# Simular llamada a API externa
response = {
"status": "success",
"transaction_id": "txn_123456",
"amount": amount_in_cents / 100
}
return response
# Adapter para la API externa
class AdaptadorPagoExterno(ProveedorPagos):
def __init__(self, api_externa):
self.api_externa = api_externa
def procesar_pago(self, monto, tarjeta, descripcion):
# Convertir parámetros al formato de la API externa
amount_in_cents = int(monto * 100)
metadata = {"description": descripcion}
# Llamar a la API externa
resultado = self.api_externa.charge(amount_in_cents, tarjeta, metadata)
# Convertir respuesta al formato esperado
return {
"exitoso": resultado["status"] == "success",
"id_transaccion": resultado["transaction_id"],
"monto": resultado["amount"]
}
# Uso
api_externa = ApiPagoExterna()
procesador_pagos = AdaptadorPagoExterno(api_externa)
resultado = procesador_pagos.procesar_pago(99.99, "tok_visa_123", "Compra en tienda")
print(resultado)
Paso 3: Decorator - Añadir funcionalidad dinámicamente
El patrón Decorator permite añadir responsabilidades adicionales a un objeto de manera dinámica, proporcionando una alternativa flexible a la herencia para extender funcionalidades.
Implementación básica
from abc import ABC, abstractmethod
# Componente base
class Bebida(ABC):
@abstractmethod
def costo(self):
pass
@abstractmethod
def descripcion(self):
pass
# Componente concreto
class Cafe(Bebida):
def costo(self):
return 2.0
def descripcion(self):
return "Café"
# Decorator base
class DecoradorBebida(Bebida):
def __init__(self, bebida):
self.bebida = bebida
def costo(self):
return self.bebida.costo()
def descripcion(self):
return self.bebida.descripcion()
# Decorators concretos
class Leche(DecoradorBebida):
def costo(self):
return self.bebida.costo() + 0.5
def descripcion(self):
return self.bebida.descripcion() + " con leche"
class Azucar(DecoradorBebida):
def costo(self):
return self.bebida.costo() + 0.2
def descripcion(self):
return self.bebida.descripcion() + " con azúcar"
class Canela(DecoradorBebida):
def costo(self):
return self.bebida.costo() + 0.3
def descripcion(self):
return self.bebida.descripcion() + " con canela"
# Uso
bebida = Cafe()
print(f"{bebida.descripcion()}: ${bebida.costo()}")
bebida = Leche(bebida)
print(f"{bebida.descripcion()}: ${bebida.costo()}")
bebida = Azucar(bebida)
print(f"{bebida.descripcion()}: ${bebida.costo()}")
bebida = Canela(bebida)
print(f"{bebida.descripcion()}: ${bebida.costo()}")
Decorator funcional (usando funciones)
def decorador_logging(func):
def wrapper(*args, **kwargs):
print(f"Llamando a {func.__name__} con args: {args}, kwargs: {kwargs}")
resultado = func(*args, **kwargs)
print(f"Resultado: {resultado}")
return resultado
return wrapper
def decorador_caching(func):
cache = {}
def wrapper(*args, **kwargs):
clave = str(args) + str(sorted(kwargs.items()))
if clave not in cache:
print(f"Calculando resultado para {clave}")
cache[clave] = func(*args, **kwargs)
else:
print(f"Usando resultado cacheado para {clave}")
return cache[clave]
return wrapper
@decorador_logging
@decorador_caching
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Uso
print(fibonacci(5))
print(fibonacci(5)) # Usará cache
Decorator para logging y timing
import time
from functools import wraps
def timing_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} tomó {end_time - start_time:.4f} segundos")
return result
return wrapper
def logging_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Llamando {func.__name__} con argumentos: {args}")
result = func(*args, **kwargs)
print(f"{func.__name__} retornó: {result}")
return result
return wrapper
@timing_decorator
@logging_decorator
def proceso_complejo(n):
time.sleep(0.1) # Simular trabajo
return sum(range(n))
# Uso
resultado = proceso_complejo(1000)
Paso 4: Facade - Interfaz simplificada
El patrón Facade proporciona una interfaz simplificada para un subsistema complejo, haciendo que sea más fácil de usar y entender.
Implementación básica
# Subsistema complejo
class SubsistemaBaseDatos:
def conectar(self):
return "Conectado a base de datos"
def ejecutar_query(self, query):
return f"Ejecutando: {query}"
def desconectar(self):
return "Desconectado de base de datos"
class SubsistemaCache:
def obtener(self, clave):
return f"Obteniendo {clave} del caché"
def guardar(self, clave, valor):
return f"Guardando {clave}: {valor} en caché"
class SubsistemaLogger:
def log(self, mensaje):
return f"LOG: {mensaje}"
# Facade que simplifica el uso del subsistema
class FacadeServicioDatos:
def __init__(self):
self.db = SubsistemaBaseDatos()
self.cache = SubsistemaCache()
self.logger = SubsistemaLogger()
def obtener_datos_usuario(self, user_id):
# Interfaz simplificada que oculta la complejidad
self.logger.log(f"Obteniendo datos para usuario {user_id}")
# Intentar obtener del caché primero
datos_cache = self.cache.obtener(f"user_{user_id}")
if "Usuario encontrado" in datos_cache:
return datos_cache
# Si no está en caché, obtener de BD
self.db.conectar()
query = f"SELECT * FROM usuarios WHERE id = {user_id}"
datos_db = self.db.ejecutar_query(query)
self.db.desconectar()
# Guardar en caché para futuras consultas
self.cache.guardar(f"user_{user_id}", datos_db)
return datos_db
# Uso del facade
servicio = FacadeServicioDatos()
# Interfaz simple en lugar de manejar múltiples subsistemas
datos = servicio.obtener_datos_usuario(123)
print(datos)
Facade para compilador
class AnalizadorLexico:
def analizar(self, codigo):
return f"Tokens extraídos de: {codigo[:20]}..."
class AnalizadorSintactico:
def analizar(self, tokens):
return f"Árbol sintáctico generado para: {tokens[:20]}..."
class GeneradorCodigo:
def generar(self, arbol):
return f"Código objeto generado para: {arbol[:20]}..."
class Optimizador:
def optimizar(self, codigo):
return f"Código optimizado: {codigo[:20]}..."
class FacadeCompilador:
def __init__(self):
self.lexer = AnalizadorLexico()
self.parser = AnalizadorSintactico()
self.generator = GeneradorCodigo()
self.optimizer = Optimizador()
def compilar(self, codigo_fuente, optimizar=True):
print("Iniciando compilación...")
tokens = self.lexer.analizar(codigo_fuente)
print(f"✓ Análisis léxico completado")
arbol = self.parser.analizar(tokens)
print(f"✓ Análisis sintáctico completado")
codigo = self.generator.generar(arbol)
print(f"✓ Generación de código completado")
if optimizar:
codigo = self.optimizer.optimizar(codigo)
print(f"✓ Optimización completada")
print("Compilación finalizada")
return codigo
# Uso
compilador = FacadeCompilador()
codigo = "def suma(a, b): return a + b"
resultado = compilador.compilar(codigo)
print(f"Resultado: {resultado}")
Paso 5: Proxy - Control de acceso
El patrón Proxy proporciona un sustituto o intermediario para controlar el acceso a un objeto, añadiendo lógica adicional como caching, logging, o control de acceso.
Proxy virtual (lazy loading)
from abc import ABC, abstractmethod
class Imagen(ABC):
@abstractmethod
def mostrar(self):
pass
class ImagenReal(Imagen):
def __init__(self, nombre_archivo):
self.nombre_archivo = nombre_archivo
self._cargar_imagen_desde_disco()
def _cargar_imagen_desde_disco(self):
print(f"Cargando imagen {self.nombre_archivo} desde disco...")
# Simular carga costosa
import time
time.sleep(1)
print(f"Imagen {self.nombre_archivo} cargada")
def mostrar(self):
print(f"Mostrando imagen {self.nombre_archivo}")
class ProxyImagen(Imagen):
def __init__(self, nombre_archivo):
self.nombre_archivo = nombre_archivo
self._imagen_real = None
def mostrar(self):
if self._imagen_real is None:
self._imagen_real = ImagenReal(self.nombre_archivo)
self._imagen_real.mostrar()
# Uso
imagen1 = ProxyImagen("foto1.jpg")
imagen2 = ProxyImagen("foto2.jpg")
print("Imagen 1 creada (no cargada aún)")
print("Imagen 2 creada (no cargada aún)")
imagen1.mostrar() # Carga y muestra
imagen1.mostrar() # Solo muestra (ya cargada)
imagen2.mostrar() # Carga y muestra
Proxy de protección
class ServicioBanco:
def __init__(self, saldo_inicial=1000):
self.saldo = saldo_inicial
def retirar_dinero(self, cantidad, usuario):
if cantidad <= self.saldo:
self.saldo -= cantidad
return f"Retirados ${cantidad}. Saldo restante: ${self.saldo}"
return "Saldo insuficiente"
def consultar_saldo(self, usuario):
return f"Saldo actual: ${self.saldo}"
class ProxyBanco:
def __init__(self, servicio_banco, usuario_autorizado):
self.servicio_banco = servicio_banco
self.usuario_autorizado = usuario_autorizado
def retirar_dinero(self, cantidad, usuario):
if self._verificar_acceso(usuario):
return self.servicio_banco.retirar_dinero(cantidad, usuario)
return "Acceso denegado"
def consultar_saldo(self, usuario):
if self._verificar_acceso(usuario):
return self.servicio_banco.consultar_saldo(usuario)
return "Acceso denegado"
def _verificar_acceso(self, usuario):
return usuario == self.usuario_autorizado
# Uso
banco_real = ServicioBanco()
proxy_banco = ProxyBanco(banco_real, "usuario123")
# Acceso autorizado
print(proxy_banco.consultar_saldo("usuario123"))
print(proxy_banco.retirar_dinero(200, "usuario123"))
# Acceso no autorizado
print(proxy_banco.consultar_saldo("usuario456"))
print(proxy_banco.retirar_dinero(100, "usuario456"))
Proxy de caching
class ServicioWeb:
def obtener_datos(self, url):
print(f"Haciendo petición HTTP a {url}")
# Simular petición costosa
import time
time.sleep(0.5)
return f"Datos de {url}"
class ProxyCacheWeb:
def __init__(self, servicio_web):
self.servicio_web = servicio_web
self.cache = {}
def obtener_datos(self, url):
if url in self.cache:
print(f"Obteniendo {url} del caché")
return self.cache[url]
datos = self.servicio_web.obtener_datos(url)
self.cache[url] = datos
return datos
# Uso
servicio = ServicioWeb()
proxy = ProxyCacheWeb(servicio)
# Primera petición - va al servicio real
print(proxy.obtener_datos("https://api.example.com/users"))
print()
# Segunda petición - usa caché
print(proxy.obtener_datos("https://api.example.com/users"))
Paso 6: Bridge - Separar abstracción e implementación
El patrón Bridge separa una abstracción de su implementación, permitiendo que ambas varíen independientemente.
Implementación básica
from abc import ABC, abstractmethod
# Implementación
class ImplementacionDibujo(ABC):
@abstractmethod
def dibujar_linea(self, x1, y1, x2, y2):
pass
@abstractmethod
def dibujar_circulo(self, x, y, radio):
pass
class DibujoVectorial(ImplementacionDibujo):
def dibujar_linea(self, x1, y1, x2, y2):
return f"Dibujando línea vectorial: ({x1},{y1}) -> ({x2},{y2})"
def dibujar_circulo(self, x, y, radio):
return f"Dibujando círculo vectorial: centro=({x},{y}), radio={radio}"
class DibujoRaster(ImplementacionDibujo):
def dibujar_linea(self, x1, y1, x2, y2):
return f"Dibujando línea raster: ({x1},{y1}) -> ({x2},{y2})"
def dibujar_circulo(self, x, y, radio):
return f"Dibujando círculo raster: centro=({x},{y}), radio={radio}"
# Abstracción
class Forma(ABC):
def __init__(self, implementacion_dibujo):
self.implementacion_dibujo = implementacion_dibujo
@abstractmethod
def dibujar(self):
pass
# Abstracciones refinadas
class Circulo(Forma):
def __init__(self, implementacion_dibujo, x, y, radio):
super().__init__(implementacion_dibujo)
self.x = x
self.y = y
self.radio = radio
def dibujar(self):
return self.implementacion_dibujo.dibujar_circulo(self.x, self.y, self.radio)
class Rectangulo(Forma):
def __init__(self, implementacion_dibujo, x1, y1, x2, y2):
super().__init__(implementacion_dibujo)
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
def dibujar(self):
linea1 = self.implementacion_dibujo.dibujar_linea(self.x1, self.y1, self.x2, self.y1)
linea2 = self.implementacion_dibujo.dibujar_linea(self.x2, self.y1, self.x2, self.y2)
linea3 = self.implementacion_dibujo.dibujar_linea(self.x2, self.y2, self.x1, self.y2)
linea4 = self.implementacion_dibujo.dibujar_linea(self.x1, self.y2, self.x1, self.y1)
return f"Rectángulo:\n{linea1}\n{linea2}\n{linea3}\n{linea4}"
# Uso
vectorial = DibujoVectorial()
raster = DibujoRaster()
circulo_vectorial = Circulo(vectorial, 10, 10, 5)
rectangulo_raster = Rectangulo(raster, 0, 0, 20, 10)
print(circulo_vectorial.dibujar())
print()
print(rectangulo_raster.dibujar())
Paso 7: Composite - Estructuras de árbol
El patrón Composite permite tratar objetos individuales y composiciones de objetos de manera uniforme, creando estructuras de árbol.
Implementación básica
from abc import ABC, abstractmethod
class ComponenteGrafico(ABC):
@abstractmethod
def dibujar(self):
pass
@abstractmethod
def mover(self, x, y):
pass
class Forma(ComponenteGrafico):
def __init__(self, nombre):
self.nombre = nombre
self.x = 0
self.y = 0
def dibujar(self):
return f"Dibujando {self.nombre} en ({self.x}, {self.y})"
def mover(self, x, y):
self.x = x
self.y = y
return f"Moviendo {self.nombre} a ({x}, {y})"
class GrupoGrafico(ComponenteGrafico):
def __init__(self, nombre):
self.nombre = nombre
self.componentes = []
self.x = 0
self.y = 0
def agregar(self, componente):
self.componentes.append(componente)
def remover(self, componente):
self.componentes.remove(componente)
def dibujar(self):
resultado = [f"Dibujando grupo '{self.nombre}':"]
for componente in self.componentes:
resultado.append(f" {componente.dibujar()}")
return "\n".join(resultado)
def mover(self, x, y):
self.x = x
self.y = y
resultado = [f"Moviendo grupo '{self.nombre}' a ({x}, {y}):"]
for componente in self.componentes:
# Mover relativamente
resultado.append(f" {componente.mover(componente.x + x, componente.y + y)}")
return "\n".join(resultado)
# Uso
# Crear formas individuales
circulo = Forma("Círculo")
cuadrado = Forma("Cuadrado")
triangulo = Forma("Triángulo")
# Crear grupos
grupo_formas = GrupoGrafico("Formas Básicas")
grupo_formas.agregar(circulo)
grupo_formas.agregar(cuadrado)
grupo_completo = GrupoGrafico("Dibujo Completo")
grupo_completo.agregar(grupo_formas)
grupo_completo.agregar(triangulo)
print(grupo_completo.dibujar())
print()
print(grupo_completo.mover(10, 20))
Composite para sistema de archivos
from abc import ABC, abstractmethod
class ElementoSistemaArchivos(ABC):
def __init__(self, nombre):
self.nombre = nombre
@abstractmethod
def obtener_tamano(self):
pass
@abstractmethod
def mostrar(self, indentacion=0):
pass
class Archivo(ElementoSistemaArchivos):
def __init__(self, nombre, tamano):
super().__init__(nombre)
self.tamano = tamano
def obtener_tamano(self):
return self.tamano
def mostrar(self, indentacion=0):
return " " * indentacion + f"📄 {self.nombre} ({self.tamano} KB)"
class Directorio(ElementoSistemaArchivos):
def __init__(self, nombre):
super().__init__(nombre)
self.elementos = []
def agregar(self, elemento):
self.elementos.append(elemento)
def remover(self, elemento):
self.elementos.remove(elemento)
def obtener_tamano(self):
return sum(elemento.obtener_tamano() for elemento in self.elementos)
def mostrar(self, indentacion=0):
resultado = [" " * indentacion + f"📁 {self.nombre}/"]
for elemento in self.elementos:
resultado.append(elemento.mostrar(indentacion + 1))
return "\n".join(resultado)
# Uso
# Crear archivos
archivo1 = Archivo("documento.txt", 5)
archivo2 = Archivo("imagen.jpg", 1200)
archivo3 = Archivo("codigo.py", 15)
# Crear directorios
directorio_docs = Directorio("Documentos")
directorio_docs.agregar(archivo1)
directorio_imagenes = Directorio("Imágenes")
directorio_imagenes.agregar(archivo2)
directorio_proyecto = Directorio("Proyecto")
directorio_proyecto.agregar(archivo3)
directorio_proyecto.agregar(directorio_docs)
directorio_proyecto.agregar(directorio_imagenes)
# Mostrar estructura
print(directorio_proyecto.mostrar())
print(f"\nTamaño total del proyecto: {directorio_proyecto.obtener_tamano()} KB")
Paso 8: Flyweight - Compartir objetos eficientemente
El patrón Flyweight permite compartir objetos eficientemente, reduciendo el uso de memoria cuando tienes muchos objetos similares.
Implementación básica
class TipoCaracter:
def __init__(self, fuente, tamano, color):
self.fuente = fuente
self.tamano = tamano
self.color = color
def mostrar(self, posicion):
return f"Caracter en fuente {self.fuente}, tamaño {self.tamano}, color {self.color} en posición {posicion}"
class FabricaCaracteresFlyweight:
def __init__(self):
self.caracteres = {}
def obtener_caracter(self, fuente, tamano, color):
clave = (fuente, tamano, color)
if clave not in self.caracteres:
self.caracteres[clave] = TipoCaracter(fuente, tamano, color)
print(f"Creando nuevo tipo de caracter: {clave}")
else:
print(f"Reutilizando tipo de caracter: {clave}")
return self.caracteres[clave]
class Caracter:
def __init__(self, tipo_caracter, posicion):
self.tipo_caracter = tipo_caracter
self.posicion = posicion
def mostrar(self):
return self.tipo_caracter.mostrar(self.posicion)
# Uso
fabrica = FabricaCaracteresFlyweight()
# Crear muchos caracteres con pocos tipos compartidos
caracteres = []
for i in range(100):
# Solo 3 tipos diferentes de caracteres
if i % 3 == 0:
tipo = fabrica.obtener_caracter("Arial", 12, "Negro")
elif i % 3 == 1:
tipo = fabrica.obtener_caracter("Times", 14, "Azul")
else:
tipo = fabrica.obtener_caracter("Courier", 10, "Rojo")
caracter = Caracter(tipo, i)
caracteres.append(caracter)
print(f"\nTotal de caracteres: {len(caracteres)}")
print(f"Total de tipos de caracter: {len(fabrica.caracteres)}")
print(f"Memoria ahorrada: {len(caracteres) - len(fabrica.caracteres)} objetos")
Paso 9: Comparación y selección de patrones
Cuándo usar cada patrón estructural
Patrón | Cuándo usarlo | Ventajas | Desventajas |
---|---|---|---|
Adapter | Interfaces incompatibles, legacy code | Reutilización, no modifica código existente | Añade complejidad |
Decorator | Funcionalidad dinámica, herencia múltiple | Flexible, composición | Puede crear muchas clases pequeñas |
Facade | Subsistemas complejos | Simplifica uso | Puede ocultar funcionalidad necesaria |
Proxy | Control de acceso, lazy loading | Transparente, controla acceso | Puede añadir latencia |
Bridge | Abstracción e implementación variables | Independencia, extensible | Aumenta complejidad |
Composite | Estructuras jerárquicas | Uniformidad, extensible | Puede ser overkill para estructuras simples |
Flyweight | Muchos objetos similares | Ahorra memoria | Aumenta complejidad de código |
Paso 10: Proyecto práctico - Sistema de archivos
Vamos a crear un sistema de archivos completo que utilice múltiples patrones estructurales.
from abc import ABC, abstractmethod
import os
from datetime import datetime
# Flyweight para tipos de archivo
class TipoArchivoFlyweight:
def __init__(self, extension, descripcion, icono):
self.extension = extension
self.descripcion = descripcion
self.icono = icono
def obtener_info(self):
return f"{self.icono} {self.descripcion} ({self.extension})"
class FabricaTiposArchivo:
_instance = None
_tipos = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._inicializar_tipos()
return cls._instance
@classmethod
def _inicializar_tipos(cls):
cls._tipos = {
'txt': TipoArchivoFlyweight('txt', 'Archivo de texto', '📄'),
'py': TipoArchivoFlyweight('py', 'Script Python', '🐍'),
'jpg': TipoArchivoFlyweight('jpg', 'Imagen JPEG', '🖼️'),
'pdf': TipoArchivoFlyweight('pdf', 'Documento PDF', '📕'),
'mp3': TipoArchivoFlyweight('mp3', 'Archivo de audio', '🎵'),
}
def obtener_tipo(self, extension):
return self._tipos.get(extension.lower(),
TipoArchivoFlyweight(extension, f'Archivo {extension.upper()}', '📄'))
# Composite para estructura de archivos
class ElementoSistema(ABC):
def __init__(self, nombre, ruta_padre=""):
self.nombre = nombre
self.ruta_padre = ruta_padre
self.fecha_creacion = datetime.now()
self.fecha_modificacion = datetime.now()
@property
def ruta_completa(self):
if self.ruta_padre:
return os.path.join(self.ruta_padre, self.nombre)
return self.nombre
@abstractmethod
def obtener_tamano(self):
pass
@abstractmethod
def mostrar(self, indentacion=0):
pass
class Archivo(ElementoSistema):
def __init__(self, nombre, tamano=0, ruta_padre=""):
super().__init__(nombre, ruta_padre)
self.tamano = tamano
self.extension = os.path.splitext(nombre)[1][1:] if '.' in nombre else ''
self.tipo = FabricaTiposArchivo().obtener_tipo(self.extension)
def obtener_tamano(self):
return self.tamano
def mostrar(self, indentacion=0):
espacio = " " * indentacion
info_tipo = self.tipo.obtener_info()
return f"{espacio}{info_tipo} {self.nombre} ({self.tamano} KB)"
class Directorio(ElementoSistema):
def __init__(self, nombre, ruta_padre=""):
super().__init__(nombre, ruta_padre)
self.elementos = []
def agregar(self, elemento):
elemento.ruta_padre = self.ruta_completa
self.elementos.append(elemento)
self.fecha_modificacion = datetime.now()
def remover(self, elemento):
if elemento in self.elementos:
self.elementos.remove(elemento)
self.fecha_modificacion = datetime.now()
def obtener_tamano(self):
return sum(elemento.obtener_tamano() for elemento in self.elementos)
def mostrar(self, indentacion=0):
espacio = " " * indentacion
resultado = [f"{espacio}📁 {self.nombre}/"]
for elemento in sorted(self.elementos, key=lambda x: (isinstance(x, Directorio), x.nombre)):
resultado.append(elemento.mostrar(indentacion + 1))
return "\n".join(resultado)
# Adapter para sistema de archivos real
class AdaptadorSistemaArchivos:
def __init__(self, ruta_base):
self.ruta_base = ruta_base
def cargar_directorio(self, ruta_relativa=""):
ruta_completa = os.path.join(self.ruta_base, ruta_relativa)
if not os.path.exists(ruta_completa):
raise FileNotFoundError(f"Directorio no encontrado: {ruta_completa}")
directorio = Directorio(os.path.basename(ruta_completa) or "raiz",
os.path.dirname(ruta_completa) if ruta_relativa else "")
for item in os.listdir(ruta_completa):
ruta_item = os.path.join(ruta_completa, item)
if os.path.isdir(ruta_item):
subdirectorio = self.cargar_directorio(os.path.join(ruta_relativa, item))
directorio.agregar(subdirectorio)
else:
tamano = os.path.getsize(ruta_item) // 1024 # KB
archivo = Archivo(item, tamano, ruta_completa)
directorio.agregar(archivo)
return directorio
# Facade para operaciones comunes
class FacadeSistemaArchivos:
def __init__(self, ruta_base):
self.ruta_base = ruta_base
self.adaptador = AdaptadorSistemaArchivos(ruta_base)
self.raiz = None
def cargar_sistema(self):
self.raiz = self.adaptador.cargar_directorio()
return self.raiz
def buscar_archivos(self, extension, directorio=None):
if directorio is None:
directorio = self.raiz
encontrados = []
for elemento in directorio.elementos:
if isinstance(elemento, Archivo) and elemento.extension == extension:
encontrados.append(elemento)
elif isinstance(elemento, Directorio):
encontrados.extend(self.buscar_archivos(extension, elemento))
return encontrados
def obtener_estadisticas(self, directorio=None):
if directorio is None:
directorio = self.raiz
total_archivos = 0
total_directorios = 0
tamano_total = 0
tipos_archivo = {}
def contar(elemento):
nonlocal total_archivos, total_directorios, tamano_total
if isinstance(elemento, Archivo):
total_archivos += 1
tamano_total += elemento.tamano
tipos_archivo[elemento.extension] = tipos_archivo.get(elemento.extension, 0) + 1
elif isinstance(elemento, Directorio):
total_directorios += 1
for subelemento in elemento.elementos:
contar(subelemento)
contar(directorio)
return {
'archivos': total_archivos,
'directorios': total_directorios,
'tamano_total': tamano_total,
'tipos_archivo': tipos_archivo
}
# Demo del sistema
def demo_sistema_archivos():
# Crear estructura de ejemplo
raiz = Directorio("MiProyecto")
# Crear subdirectorios
src = Directorio("src")
docs = Directorio("docs")
assets = Directorio("assets")
raiz.agregar(src)
raiz.agregar(docs)
raiz.agregar(assets)
# Agregar archivos
src.agregar(Archivo("main.py", 15))
src.agregar(Archivo("utils.py", 8))
src.agregar(Archivo("config.py", 5))
docs.agregar(Archivo("README.md", 3))
docs.agregar(Archivo("manual.pdf", 250))
assets.agregar(Archivo("logo.jpg", 1200))
assets.agregar(Archivo("icon.png", 45))
assets.agregar(Archivo("background.mp3", 3500))
# Usar facade
facade = FacadeSistemaArchivos("")
facade.raiz = raiz
print("=== Estructura del Sistema de Archivos ===")
print(raiz.mostrar())
print("\n=== Estadísticas ===")
stats = facade.obtener_estadisticas()
print(f"Archivos: {stats['archivos']}")
print(f"Directorios: {stats['directorios']}")
print(f"Tamaño total: {stats['tamano_total']} KB")
print(f"Tipos de archivo: {stats['tipos_archivo']}")
print("\n=== Búsqueda de archivos Python ===")
archivos_py = facade.buscar_archivos('py')
for archivo in archivos_py:
print(f" {archivo.ruta_completa}")
if __name__ == "__main__":
demo_sistema_archivos()
Conclusión
¡Has dominado los patrones de diseño estructurales! Estos patrones te permiten organizar y simplificar la estructura de tus aplicaciones, haciendo que el código sea más mantenible y flexible.
Practica aplicando estos patrones en proyectos reales y combina diferentes patrones según las necesidades específicas de tu aplicación.
Para más tutoriales sobre patrones de diseño estructurales avanzados, visita nuestra sección de tutoriales.
¡Sigue practicando y aplicando estos patrones en proyectos reales!
💡 Tip Importante
📝 Mejores Prácticas en Patrones Estructurales
- Elige el patrón adecuado: Analiza la estructura del problema antes de seleccionar un patrón
- Principio de responsabilidad única: Cada clase debe tener una sola razón para cambiar
- Principio abierto/cerrado: Los patrones deben estar abiertos a extensión pero cerrados a modificación
- Composición sobre herencia: Prefiere composición cuando sea posible
- Mantén la simplicidad: No uses patrones complejos para problemas simples
- Documenta tus decisiones: Explica por qué elegiste ciertos patrones en tu código
- Prueba tus estructuras: Asegúrate de que tus patrones funcionen correctamente
- Considera el rendimiento: Algunos patrones pueden afectar el rendimiento
📚 Recursos Recomendados:
- Design Patterns: Elements of Reusable Object-Oriented Software - Gang of Four
- Head First Design Patterns - Libro práctico
- Refactoring.Guru - Structural Patterns - Ejemplos interactivos
¡Estos patrones te ayudarán a crear software con mejor estructura y organización!
No hay comentarios aún
Sé el primero en comentar este tutorial.