Patrones de Diseño Estructurales
Componer objetos para crear estructuras más grandes y flexibles
Los patrones de diseño estructurales proponen formas de componer objetos para proporcionar nueva funcionalidad. Mientras que los patrones creacionales se enfocan en cómo se crean los objetos, los estructurales se concentran en cómo se organizan y conectan.
En este artículo exploraremos seis patrones fundamentales: Adapter, Decorator, Bridge, Facade, Flyweight y Proxy. Cada uno resuelve problemas específicos de composición y estructura en el diseño de software.
Adapter Pattern
El patrón Adapter es un patrón estructural que ayuda a hacer compatibles dos interfaces incompatibles. Cuando tienes un componente antiguo que quieres usar en un sistema nuevo, o viceversa, rara vez pueden comunicarse sin modificaciones. Si cambiar el código no es posible, escribimos una capa adicional que hace las modificaciones necesarias: el adaptador.
Analogía del Mundo Real
Cuando viajas de Europa a USA o UK, necesitas un adaptador de enchufe para cargar tu laptop. El mismo concepto aplica para conectar dispositivos a tu computadora: el adaptador USB.
Casos de Uso
Ejemplo 1: Adaptar Sistema de Pagos Legacy
# Sistema de pagos legacy class OldPaymentSystem: def __init__(self, currency): self.currency = currency def make_payment(self, amount): print(f"[OLD] Pay {amount} {self.currency}") # Nuevo sistema de pagos con interfaz diferente class NewPaymentGateway: def __init__(self, currency): self.currency = currency def execute_payment(self, amount): print(f"Execute payment of {amount} {self.currency}") # El Adapter hace compatible NewPaymentGateway con la interfaz esperada class PaymentAdapter: def __init__(self, system): self.system = system # El adaptee def make_payment(self, amount): self.system.execute_payment(amount) # Uso if __name__ == "__main__": new_system = NewPaymentGateway("euro") adapter = PaymentAdapter(new_system) adapter.make_payment(100) # Usa la interfaz esperada
Ejemplo 2: Interfaz Unificada para Múltiples Clases
# Clase del club con interfaz esperada class Club: def __init__(self, name): self.name = name def __str__(self): return f"the club {self.name}" def organize_event(self): return "hires an artist to perform" # Clases externas con interfaces diferentes class Musician: def __init__(self, name): self.name = name def __str__(self): return f"the musician {self.name}" def play(self): return "plays music" class Dancer: def __init__(self, name): self.name = name def __str__(self): return f"the dancer {self.name}" def dance(self): return "does a dance performance" # Adapter genérico para cualquier objeto class Adapter: def __init__(self, obj, adapted_methods): self.obj = obj self.__dict__.update(adapted_methods) def __str__(self): return str(self.obj) # Uso con adaptación dinámica def main(): objects = [ Club("Jazz Cafe"), Musician("Roy Ayers"), Dancer("Shane Sparks"), ] for obj in objects: if hasattr(obj, "play"): obj = Adapter(obj, dict(organize_event=obj.play)) elif hasattr(obj, "dance"): obj = Adapter(obj, dict(organize_event=obj.dance)) print(f"{obj} {obj.organize_event()}")
the musician Roy Ayers plays music
the dancer Shane Sparks does a dance performance
Decorator Pattern
El patrón Decorator permite agregar responsabilidades a un objeto dinámicamente, de manera transparente y sin afectar otros objetos. Python tiene una característica incorporada de decoradores que extiende este concepto aún más.
Beneficios
Implementación: Memoization Decorator
Un caso de uso clásico es la memoización: cachear resultados de funciones costosas para evitar recálculos.
import functools def memoize(func): """Decorator que cachea resultados de funciones""" cache = dict() @functools.wraps(func) def memoized(*args): if args not in cache: cache[args] = func(*args) return cache[args] return memoized @memoize def number_sum(n): """Returns the sum of the first n numbers""" print(f"Calculating sum for {n}...") return sum(range(n + 1)) @memoize def fibonacci(n): """Returns the nth Fibonacci number""" if n in (0, 1): return n return fibonacci(n - 1) + fibonacci(n - 2) # Uso if __name__ == "__main__": # Primera llamada: calcula print(f"Sum: {number_sum(100)}") # Segunda llamada: usa cache print(f"Sum (cached): {number_sum(100)}") print(f"Fibonacci(30): {fibonacci(30)}")
Sum: 5050
Sum (cached): 5050
Fibonacci(30): 832040
@functools.wraps(func) preserva los atributos __name__ y __doc__ de la función original. Sin él, la función decorada perdería su identidad.
Bridge Pattern
Mientras que el Adapter se usa después para hacer funcionar clases incompatibles, el Bridge se diseña desde el inicio para desacoplar una abstracción de su implementación, permitiendo que ambas varíen independientemente.
Casos de Uso
Implementación: Sistema de Extracción de Contenido
from abc import ABC, abstractmethod import urllib.request # Interfaz del Implementor class ResourceContentFetcher(ABC): @abstractmethod def fetch(self, path: str) -> str: pass # Implementación concreta: URL class URLFetcher(ResourceContentFetcher): def fetch(self, path: str) -> str: req = urllib.request.Request(path) with urllib.request.urlopen(req) as response: return response.read().decode('utf-8') # Implementación concreta: Archivo Local class LocalFileFetcher(ResourceContentFetcher): def fetch(self, path: str) -> str: with open(path, 'r') as f: return f.read() # La Abstracción class ResourceContent: def __init__(self, fetcher: ResourceContentFetcher): self.fetcher = fetcher # Bridge al implementor def show_content(self, path: str): content = self.fetcher.fetch(path) print(f"Fetched content with {len(content)} characters") # Uso if __name__ == "__main__": # Mismo ResourceContent, diferentes implementaciones url_resource = ResourceContent(URLFetcher()) url_resource.show_content("https://python.org") local_resource = ResourceContent(LocalFileFetcher()) local_resource.show_content("config.txt")
Fetched content with 1327 characters
ResourceContent. Abstracción e implementación evolucionan independientemente.
Facade Pattern
A medida que los sistemas evolucionan, pueden volverse muy complejos. El patrón Facade nos ayuda a ocultar la complejidad interna y exponer solo lo necesario a través de una interfaz simplificada. Es una capa de abstracción sobre un sistema existente complejo.
Analogía del Mundo Real
Una computadora es un facade: todo lo que necesitas hacer es presionar un botón para encenderla. Toda la complejidad del hardware (BIOS, boot loader, kernel) se maneja de forma transparente. Lo mismo aplica a la llave de tu auto o al servicio al cliente de un banco.
Beneficios
Implementación: Sistema Operativo Multi-Server
from abc import ABC, abstractmethod from enum import Enum State = Enum("State", "NEW RUNNING SLEEPING RESTART ZOMBIE") # Clases internas del sistema (ocultas al cliente) class User: pass class Process: pass class File: pass # Servidor abstracto class Server(ABC): @abstractmethod def boot(self): pass @abstractmethod def kill(self, restart=True): pass # Servidores específicos class FileServer(Server): def boot(self): print("booting the FileServer") def kill(self, restart=True): print("Killing FileServer") def create_file(self, user, name, perms): print(f"creating file '{name}' for user '{user}' with {perms}") class ProcessServer(Server): def boot(self): print("booting the ProcessServer") def kill(self, restart=True): print("Killing ProcessServer") def create_process(self, user, name): print(f"creating process '{name}' for user '{user}'") # El FACADE: interfaz simple para el sistema complejo class OperatingSystem: """The Facade - oculta la complejidad del sistema""" def __init__(self): self.fs = FileServer() self.ps = ProcessServer() def start(self): """Único método que el cliente necesita""" [server.boot() for server in (self.fs, self.ps)] def create_file(self, user, name, perms): return self.fs.create_file(user, name, perms) def create_process(self, user, name): return self.ps.create_process(user, name) # Uso: el cliente solo conoce el Facade if __name__ == "__main__": os = OperatingSystem() os.start() os.create_file("foo", "hello.txt", "-rw-r-r") os.create_process("bar", "ls /tmp")
booting the ProcessServer
creating file 'hello.txt' for user 'foo' with -rw-r-r
creating process 'ls /tmp' for user 'bar'
OperatingSystem.
Flyweight Pattern
El patrón Flyweight es una técnica de optimización para minimizar el uso de memoria compartiendo datos entre objetos similares. Un flyweight contiene datos inmutables (intrínsecos) compartidos. Los datos mutables (extrínsecos) deben proporcionarse explícitamente por el código cliente.
Ejemplo del Mundo Real
En juegos FPS como Counter-Strike, todos los soldados del mismo equipo comparten la misma representación visual. En lugar de crear objetos únicos para cada soldado, compartimos los datos comunes (modelo 3D, animaciones) y solo variamos los datos específicos (posición, salud, armas).
Requisitos para usar Flyweight (GoF)
Implementación: Renderer de Autos
import random from enum import Enum CarType = Enum("CarType", "SUBCOMPACT COMPACT SUV") class Car: # Pool compartido (atributo de clase) pool = dict() def __new__(cls, car_type): # Reutilizar objeto existente o crear nuevo obj = cls.pool.get(car_type, None) if not obj: obj = object.__new__(cls) cls.pool[car_type] = obj obj.car_type = car_type return obj def render(self, color, x, y): # color, x, y son datos extrínsecos (mutables) car_type = self.car_type print(f"render a {color} {car_type.name} car at ({x}, {y})") def main(): colors = ["white", "black", "silver", "gray", "red", "blue", "brown", "beige", "yellow", "green"] car_counter = 0 # Renderizar 10 SUBCOMPACTs for _ in range(10): c = Car(CarType.SUBCOMPACT) c.render(random.choice(colors), random.randint(0, 100), random.randint(0, 100)) car_counter += 1 # Renderizar 3 COMPACTs for _ in range(3): c = Car(CarType.COMPACT) c.render(random.choice(colors), random.randint(0, 100), random.randint(0, 100)) car_counter += 1 # Renderizar 5 SUVs for _ in range(5): c = Car(CarType.SUV) c.render(random.choice(colors), random.randint(0, 100), random.randint(0, 100)) car_counter += 1 print(f"\ncars rendered: {car_counter}") print(f"cars actually created: {len(Car.pool)}") # Demostrar que objetos del mismo tipo son idénticos c1 = Car(CarType.SUBCOMPACT) c2 = Car(CarType.SUBCOMPACT) c3 = Car(CarType.SUV) print(f"\n{id(c1)} == {id(c2)}? {id(c1) == id(c2)}") print(f"{id(c2)} == {id(c3)}? {id(c2) == id(c3)}") if __name__ == "__main__": main()
render a black SUBCOMPACT car at (31, 99)
render a brown SUBCOMPACT car at (16, 74)
...
render a green SUV car at (16, 51)
cars rendered: 18
cars actually created: 3
4493672400 == 4493672400? True
4493672400 == 4493457488? False
Proxy Pattern
El patrón Proxy usa un objeto sustituto (proxy) para realizar acciones importantes antes de acceder al objeto real. Hay cuatro tipos principales de proxy, cada uno resolviendo problemas específicos.
Tipos de Proxy
Implementación 1: Virtual Proxy (Lazy Loading)
class LazyProperty: """Descriptor para lazy initialization""" def __init__(self, method): self.method = method self.method_name = method.__name__ def __get__(self, obj, cls): if not obj: return None # Calcular valor solo en primer acceso value = self.method(obj) # Reemplazar método con el valor calculado setattr(obj, self.method_name, value) return value class Test: def __init__(self): self.x = "foo" self.y = "bar" self._resource = None @LazyProperty def resource(self): print("initializing self._resource (expensive!)") self._resource = tuple(range(5)) return self._resource # Uso if __name__ == "__main__": t = Test() print(t.x, t.y) # No inicializa resource print(t.resource) # Ahora sí inicializa print(t.resource) # Usa valor cacheado
initializing self._resource (expensive!)
(0, 1, 2, 3, 4)
(0, 1, 2, 3, 4)
Implementación 2: Protection Proxy
class SensitiveInfo: """Recurso protegido""" def __init__(self): self.users = ["nick", "tom", "ben", "mike"] def read(self): nb = len(self.users) print(f"There are {nb} users: {' '.join(self.users)}") def add(self, user): self.users.append(user) print(f"Added user {user}") class Info: """Protection Proxy""" def __init__(self): self.protected = SensitiveInfo() self.secret = "0xdeadbeef" def read(self): # Lectura permitida sin autenticación self.protected.read() def add(self, user): # Escritura requiere autenticación sec = input("what is the secret? ") if sec == self.secret: self.protected.add(user) else: print("That's wrong!") # Uso if __name__ == "__main__": info = Info() info.read() # Sin restricción info.add("alice") # Requiere secreto
Implementación 3: Smart Proxy (Reference Counting)
from typing import Protocol class DBConnectionInterface(Protocol): def exec_query(self, query): ... class DBConnection: def __init__(self): print("DB connection created") def exec_query(self, query): return f"Executing query: {query}" def close(self): print("DB connection closed") class SmartProxy: """Maneja reference counting automáticamente""" def __init__(self): self.cnx = None self.ref_count = 0 def access_resource(self): if self.cnx is None: self.cnx = DBConnection() self.ref_count += 1 print(f"DB now has {self.ref_count} references") def exec_query(self, query): if self.cnx is None: self.access_resource() result = self.cnx.exec_query(query) print(result) self.release_resource() return result def release_resource(self): if self.ref_count > 0: self.ref_count -= 1 print(f"{self.ref_count} remaining refs") if self.ref_count == 0 and self.cnx is not None: self.cnx.close() self.cnx = None # Uso if __name__ == "__main__": proxy = SmartProxy() proxy.exec_query("SELECT * FROM users") proxy.exec_query("UPDATE users SET name = 'John'")
DB now has 1 references
Executing query: SELECT * FROM users
0 remaining refs
DB connection closed
DB connection created
DB now has 1 references
Executing query: UPDATE users SET name = 'John'
0 remaining refs
DB connection closed
Resumen de Patrones
01 // Adapter
Hace compatibles interfaces incompatibles. Úsalo para integrar sistemas legacy o librerías externas.
02 // Decorator
Agrega responsabilidades dinámicamente sin herencia. Ideal para cross-cutting concerns.
03 // Bridge
Desacopla abstracción de implementación desde el inicio. Ambas pueden variar independientemente.
04 // Facade
Proporciona una interfaz simple a un sistema complejo. Oculta la complejidad interna.
05 // Flyweight
Minimiza memoria compartiendo datos inmutables. Optimización para muchos objetos similares.
06 // Proxy
Objeto sustituto que controla acceso al real. Virtual, Protection, Remote y Smart variants.