Patrones de Diseño de Comportamiento
Patrones que gestionan la interconexión de objetos y algoritmos
Los patrones de diseño de comportamiento se enfocan en la comunicación entre objetos, definiendo cómo interactúan y distribuyen responsabilidades. Estos patrones ayudan a crear sistemas más flexibles y mantenibles al establecer patrones claros de colaboración entre componentes.
En este artículo exploraremos nueve patrones fundamentales: Chain of Responsibility, Command, Observer, State, Interpreter, Strategy, Memento, Iterator y Template.
- 01 // Chain of Responsibility
- 02 // Command
- 03 // Observer
- 04 // State
- 05 // Interpreter
- 06 // Strategy
- 07 // Memento
- 08 // Iterator
- 09 // Template
Chain of Responsibility
El patrón Chain of Responsibility ofrece una forma elegante de manejar solicitudes pasándolas a través de una cadena de manejadores. Cada manejador decide si puede procesar la solicitud o si debe delegarla al siguiente en la cadena.
Beneficios
Sistemas de aprobación de compras con múltiples niveles
Filtros y middleware en frameworks web
Sistemas basados en eventos donde múltiples objetos pueden manejar un evento
Sistema de Eventos con Widgets
class Event: def __init__(self, name): self.name = name def __str__(self): return self.name class Widget: def __init__(self, parent=None): self.parent = parent def handle(self, event): # Despacho dinámico usando hasattr/getattr handler = f"handle_{event}" if hasattr(self, handler): method = getattr(self, handler) method(event) elif self.parent is not None: self.parent.handle(event) else: print(f"Unhandled event: {event}") class MainWindow(Widget): def handle_close(self, event): print("MainWindow: close") class SendDialog(Widget): def handle_paint(self, event): print("SendDialog: paint") class MsgText(Widget): def handle_down(self, event): print("MsgText: down") # Uso if __name__ == "__main__": mw = MainWindow() sd = SendDialog(mw) msg = MsgText(sd) for e in ("down", "paint", "close"): evt = Event(e) print(f"Sending event -{evt}- to MsgText") msg.handle(evt)
MsgText: down
Sending event -paint- to MsgText
SendDialog: paint
Sending event -close- to MsgText
MainWindow: close
down es manejado por MsgText, paint sube a SendDialog, y close llega hasta MainWindow. Cada evento encuentra su manejador apropiado en la cadena.
Command
El patrón Command encapsula una operación (undo, redo, copy, paste, etc.) como un objeto. Esto permite parametrizar clientes con colas, solicitudes y operaciones, además de soportar operaciones reversibles.
Beneficios
Utilidades de Archivos con Undo
import os import logging logging.basicConfig(level=logging.DEBUG) class RenameFile: def __init__(self, src, dest): self.src = src self.dest = dest def execute(self): logging.info(f"[renaming '{self.src}' to '{self.dest}']") os.rename(self.src, self.dest) def undo(self): logging.info(f"[renaming '{self.dest}' back to '{self.src}']") os.rename(self.dest, self.src) class CreateFile: def __init__(self, path, txt="hello world\n"): self.path = path self.txt = txt def execute(self): logging.info(f"[creating file '{self.path}']") with open(self.path, "w") as f: f.write(self.txt) def undo(self): logging.info(f"[deleting file {self.path}]") os.remove(self.path) class ReadFile: def __init__(self, path): self.path = path def execute(self): logging.info(f"[reading file '{self.path}']") with open(self.path) as f: print(f.read(), end="") # Uso if __name__ == "__main__": commands = [ CreateFile("file1"), ReadFile("file1"), RenameFile("file1", "file2"), ] for c in commands: c.execute() # Undo en orden inverso for c in reversed(commands): try: c.undo() except AttributeError as e: logging.error(str(e))
INFO:[reading file 'file1']
hello world
INFO:[renaming 'file1' to 'file2']
INFO:[renaming 'file2' back to 'file1']
ERROR:'ReadFile' object has no attribute 'undo'
INFO:[deleting file file1]
ReadFile no tiene método undo() porque leer un archivo no es una operación reversible. El manejo de excepciones permite que la cadena de undo continúe gracefully.
Observer
El patrón Observer describe una relación publish-subscribe entre un objeto (publisher/subject) y múltiples objetos (subscribers/observers). El subject notifica a los subscribers sobre cambios de estado.
Beneficios
Sistema de Monitoreo Climático
class Observer: """Interfaz base para observers""" def update(self, temperature, humidity, pressure): pass class WeatherStation: """Subject que mantiene lista de observers""" def __init__(self): self.observers = [] def add_observer(self, observer): self.observers.append(observer) def remove_observer(self, observer): self.observers.remove(observer) def set_weather_data(self, temp, humidity, pressure): for observer in self.observers: observer.update(temp, humidity, pressure) class DisplayDevice(Observer): def __init__(self, name): self.name = name def update(self, temp, humidity, pressure): print(f"{self.name} Display") print(f" Temp: {temp}°C | Humidity: {humidity}%") class WeatherApp(Observer): def __init__(self, name): self.name = name def update(self, temp, humidity, pressure): print(f"{self.name} App - Weather Alert!") print(f" Pressure: {pressure}hPa") # Uso if __name__ == "__main__": station = WeatherStation() display1 = DisplayDevice("Living Room") display2 = DisplayDevice("Bedroom") app = WeatherApp("Mobile") station.add_observer(display1) station.add_observer(display2) station.add_observer(app) print("=== Weather Update ===") station.set_weather_data(25.5, 60, 1013.2)
Living Room Display
Temp: 25.5°C | Humidity: 60%
Bedroom Display
Temp: 25.5°C | Humidity: 60%
Mobile App - Weather Alert!
Pressure: 1013.2hPa
remove_observer() para dejar de recibir actualizaciones dinámicamente.
State
El patrón State implementa una máquina de estados para resolver problemas de ingeniería de software. Una máquina de estados solo puede tener un estado activo a la vez, y las transiciones cambian de un estado a otro.
Componentes Clave
Máquina de Estados con state_machine
from state_machine import ( State, Event, acts_as_state_machine, after, before ) @acts_as_state_machine class Process: # Definir estados created = State(initial=True) waiting = State() running = State() terminated = State() blocked = State() # Definir transiciones wait = Event( from_states=(created, running, blocked), to_state=waiting ) run = Event(from_states=waiting, to_state=running) terminate = Event(from_states=running, to_state=terminated) block = Event(from_states=running, to_state=blocked) def __init__(self, name): self.name = name @after("wait") def wait_info(self): print(f"{self.name} entered waiting mode") @after("run") def run_info(self): print(f"{self.name} is running") @before("terminate") def terminate_info(self): print(f"{self.name} terminated") # Uso if __name__ == "__main__": p1 = Process("Process_1") print(f"State: {p1.current_state}") p1.wait() p1.run() p1.terminate()
Process_1 entered waiting mode
Process_1 is running
Process_1 terminated
pip install state_machine. Las transiciones ilegales (como created → terminated) fallan gracefully sin crashear la aplicación.
Interpreter
El patrón Interpreter se usa para crear un Domain-Specific Language (DSL) interno. Permite ofrecer un framework de programación simple a expertos de dominio sin exponer las complejidades del lenguaje host.
Características
DSL para Casa Inteligente
from pyparsing import Word, alphanums, Suppress, OneOrMore class Gate: def open(self): print("Opening gate...") def close(self): print("Closing gate...") class Boiler: def __init__(self): self.temp = 20 def increase(self, degrees): self.temp += degrees print(f"Boiler temp: {self.temp}°C") def parse_and_execute(dsl_input, devices): # Definir gramática word = Word(alphanums) token = Suppress("->") command = OneOrMore(word) grammar = command + token + word + token + command for line in dsl_input.splitlines(): if not line.strip(): continue result = grammar.parseString(line) cmd, receiver, *args = result device = devices.get(receiver) if device and hasattr(device, cmd): method = getattr(device, cmd) if args: method(int(args[0])) else: method() # Uso if __name__ == "__main__": devices = {"gate": Gate(), "boiler": Boiler()} dsl = """ open -> gate -> now increase -> boiler -> 5 close -> gate -> now """ parse_and_execute(dsl, devices)
Boiler temp: 25°C
Closing gate...
pip install pyparsing. El patrón Interpreter es ideal para DSLs simples; para lenguajes complejos usa herramientas como ANTLR o Bison.
Strategy
El patrón Strategy permite usar múltiples soluciones para el mismo problema de forma transparente. Permite cambiar dinámicamente el algoritmo usado sin modificar el código cliente.
Beneficios
Verificar Caracteres Únicos
def pairs(seq): """Genera pares de elementos adyacentes""" n = len(seq) for i in range(n): yield seq[i], seq[(i + 1) % n] def allUniqueSort(s): """Estrategia 1: Ordenar y comparar pares""" if len(s) < 2: return True str_sorted = sorted(s) for c1, c2 in pairs(str_sorted): if c1 == c2: return False return True def allUniqueSet(s): """Estrategia 2: Usar un set""" return len(s) == len(set(s)) def check_unique(word, strategy): """Contexto que usa la estrategia seleccionada""" result = strategy(word) print(f"'{word}' - All unique: {result} ({strategy.__name__})") return result # Uso if __name__ == "__main__": words = ["dream", "pizza", "1r2a3ae"] print("=== Using Set Strategy ===") for word in words: check_unique(word, allUniqueSet) print("\n=== Using Sort Strategy ===") for word in words: check_unique(word, allUniqueSort)
'dream' - All unique: True (allUniqueSet)
'pizza' - All unique: False (allUniqueSet)
'1r2a3ae' - All unique: False (allUniqueSet)
=== Using Sort Strategy ===
'dream' - All unique: True (allUniqueSort)
'pizza' - All unique: False (allUniqueSort)
'1r2a3ae' - All unique: False (allUniqueSort)
Memento
El patrón Memento permite capturar y almacenar el estado interno de un objeto para poder restaurarlo posteriormente. Es fundamental para implementar funcionalidad de undo/redo.
Componentes
Sistema de Citas con Undo
import pickle class Quote: def __init__(self, text, author): self.text = text self.author = author def save_state(self): """Crea un memento del estado actual""" return pickle.dumps(self.__dict__) def restore_state(self, memento): """Restaura el estado desde un memento""" previous = pickle.loads(memento) self.__dict__.clear() self.__dict__.update(previous) def __str__(self): return f'"{self.text}" - {self.author}' # Uso if __name__ == "__main__": quote = Quote( "A room without books is like a body without a soul.", "Unknown" ) print("Original:") print(quote) # Guardar estado saved = quote.save_state() # Modificar quote.author = "Marcus Tullius Cicero" print("\nAfter update:") print(quote) # Restaurar (Undo) quote.restore_state(saved) print("\nAfter undo:") print(quote)
"A room without books is like a body without a soul." - Unknown
After update:
"A room without books is like a body without a soul." - Marcus Tullius Cicero
After undo:
"A room without books is like a body without a soul." - Unknown
pickle se usa aquí para demostración. En producción, considera alternativas más seguras ya que pickle puede ejecutar código arbitrario al deserializar.
Iterator
El patrón Iterator proporciona una forma de acceder secuencialmente a los elementos de una colección sin exponer su representación subyacente. En Python, Iterator es una característica del lenguaje incorporada.
Características Python
Equipo de Fútbol Iterable
class FootballTeamIterator: """Iterator para el equipo de fútbol""" def __init__(self, members): self.members = members self.index = 0 def __iter__(self): return self def __next__(self): if self.index < len(self.members): member = self.members[self.index] self.index += 1 return member raise StopIteration class FootballTeam: """Contenedor iterable""" def __init__(self, name): self.name = name self.members = [] def add_player(self, player): self.members.append(player) def add_coach(self, coach): self.members.append(coach) def __iter__(self): return FootballTeamIterator(self.members) # Uso if __name__ == "__main__": team = FootballTeam("Dream Team") for i in range(1, 12): team.add_player(f"player{i}") team.add_coach("coach1") team.add_coach("coach2") print(f"=== {team.name} ===") for member in team: print(member)
player1
player2
player3
...
player11
coach1
coach2
StopIteration en los bucles for. El protocolo iterator permite usar tu clase con list comprehensions, funciones como list(), sum(), etc.
Template
El patrón Template elimina redundancia de código al definir el esqueleto de un algoritmo, delegando algunos pasos a subclases. Permite redefinir partes del algoritmo sin cambiar su estructura general.
Componentes
Generador de Banners
from cowpy import cow def generate_banner(msg, style_func): """Template method: estructura fija, estilo variable""" print("=" * 50) # Invariante: header style_func(msg) # Variante: estilo personalizado print("=" * 50) # Invariante: footer def dots_style(msg): """Estilo con puntos""" print(msg.replace(" ", ".").upper()) def admire_style(msg): """Estilo con exclamaciones""" print(f"!!! {msg.upper()} !!!") def cow_style(msg): """Estilo con ASCII art de vaca""" cheese = cow.Moose() print(cheese.milk(msg)) # Uso if __name__ == "__main__": message = "Happy Coding" print("\n>>> Dots Style:") generate_banner(message, dots_style) print("\n>>> Admire Style:") generate_banner(message, admire_style) print("\n>>> Cow Style:") generate_banner(message, cow_style)
==================================================
HAPPY.CODING
==================================================
>>> Admire Style:
==================================================
!!! HAPPY CODING !!!
==================================================
>>> Cow Style:
==================================================
____________
( Happy Coding )
------------
\ \_\_ _/_/
...
==================================================
pip install cowpy. El Template pattern es especialmente útil cuando tienes algoritmos con estructura similar pero detalles diferentes.
Resumen de Patrones
01 // Chain of Responsibility
Pasa solicitudes a través de una cadena de manejadores hasta encontrar uno que pueda procesarla.
02 // Command
Encapsula operaciones como objetos, permitiendo undo/redo y ejecución diferida.
03 // Observer
Define una relación publish-subscribe para notificar cambios de estado a múltiples objetos.
04 // State
Implementa máquinas de estados para gestionar comportamiento basado en estados internos.
05 // Interpreter
Crea DSLs internos para ofrecer interfaces de programación simplificadas.
06 // Strategy
Permite intercambiar algoritmos dinámicamente sin modificar el código cliente.
07 // Memento
Captura y restaura el estado interno de objetos para implementar undo.
08 // Iterator
Proporciona acceso secuencial a elementos de una colección sin exponer su estructura.
09 // Template
Define el esqueleto de un algoritmo, delegando pasos específicos a subclases.