jueves, 15 de enero de 2026

Principios Fundamentales de Diseño en Python - Parte 5 - Patrones de Diseño de Comportamiento

Patrones de Diseño de Comportamiento en Python
Python // Design Patterns

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.

// Contenido
  • 01 // Chain of Responsibility
  • 02 // Command
  • 03 // Observer
  • 04 // State
  • 05 // Interpreter
  • 06 // Strategy
  • 07 // Memento
  • 08 // Iterator
  • 09 // Template
PATTERN_01

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

DESACOPLAMIENTO
El cliente solo conoce el inicio de la cadena
FLEXIBILIDAD
Añadir o remover manejadores dinámicamente
SRP
Cada manejador tiene una única responsabilidad
Flujo de la Cadena
Request
Handler A
Handler B
Handler C
Casos de Uso

    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

chain.py
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)
python chain.py
Sending event -down- to MsgText
MsgText: down
Sending event -paint- to MsgText
SendDialog: paint
Sending event -close- to MsgText
MainWindow: close
El evento down es manejado por MsgText, paint sube a SendDialog, y close llega hasta MainWindow. Cada evento encuentra su manejador apropiado en la cadena.
PATTERN_02

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

UNDO/REDO
Implementar operaciones reversibles fácilmente
DESACOPLAMIENTO
El invocador no conoce detalles de implementación
COMPOSICIÓN
Agrupar múltiples comandos en secuencia
Estructura del Patrón Command
Command Interface
CreateFile
RenameFile
ReadFile

Utilidades de Archivos con Undo

command.py
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))
python command.py
INFO:[creating file 'file1']
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.
PATTERN_03

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

DESACOPLAMIENTO
Publisher y subscribers independientes
DINÁMICO
Añadir/remover observers en runtime
BROADCAST
Un evento notifica a múltiples listeners
Arquitectura Observer
WeatherStation
— notifica →
Display A
Display B
WeatherApp

Sistema de Monitoreo Climático

observer.py
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)
python observer.py
=== Weather Update ===
Living Room Display
  Temp: 25.5°C | Humidity: 60%
Bedroom Display
  Temp: 25.5°C | Humidity: 60%
Mobile App - Weather Alert!
  Pressure: 1013.2hPa
Los observers están débilmente acoplados con el subject. Puedes usar remove_observer() para dejar de recibir actualizaciones dinámicamente.
PATTERN_04

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

ESTADOS
Condiciones discretas del sistema
TRANSICIONES
Cambios de un estado a otro
ACCIONES
Código ejecutado antes/después de transiciones
Diagrama de Estados de un Proceso
Created
Waiting
Running
Blocked
Terminated

Máquina de Estados con state_machine

state.py
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()
python state.py
State: created
Process_1 entered waiting mode
Process_1 is running
Process_1 terminated
Requiere instalar: pip install state_machine. Las transiciones ilegales (como created → terminated) fallan gracefully sin crashear la aplicación.
PATTERN_05

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 INTERNO
Construido sobre el lenguaje host (Python)
LEGIBILIDAD
Sintaxis más humana y específica del dominio
PARSING
Usa herramientas como pyparsing
Gramática BNF del DSL
event ::= command → receiver → arguments
open → gate
increase → boiler temp → 3 degrees

DSL para Casa Inteligente

interpreter.py
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)
python interpreter.py
Opening gate...
Boiler temp: 25°C
Closing gate...
Requiere instalar: pip install pyparsing. El patrón Interpreter es ideal para DSLs simples; para lenguajes complejos usa herramientas como ANTLR o Bison.
PATTERN_06

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

INTERCAMBIABLE
Cambiar algoritmos en runtime
ENCAPSULACIÓN
Cada estrategia está aislada
PYTHONIC
Usa funciones como first-class citizens
Selección de Estrategia
Input Data
Strategy Selector
allUniqueSet()
allUniqueSort()

Verificar Caracteres Únicos

strategy.py
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)
python strategy.py
=== Using Set Strategy ===
'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)
En Python, las funciones son objetos de primera clase, lo que simplifica enormemente la implementación del patrón Strategy. No necesitas crear clases separadas para cada estrategia.
PATTERN_07

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

MEMENTO
Almacena el estado del objeto
ORIGINATOR
Crea y restaura mementos
CARETAKER
Gestiona los mementos almacenados
Flujo de Memento
Quote Object
— save →
Memento (pickle)
— restore →
Quote Object

Sistema de Citas con Undo

memento.py
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)
python memento.py
Original:
"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
El módulo 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.
PATTERN_08

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

PROTOCOLO
__iter__() y __next__() methods
FOR LOOPS
Integración nativa con el lenguaje
GENERATORS
Forma simplificada de crear iteradores
Protocolo Iterator de Python
Iterable
— __iter__() →
Iterator
— __next__() →
Element

Equipo de Fútbol Iterable

iterator.py
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)
python iterator.py
=== Dream Team ===
player1
player2
player3
...
player11
coach1
coach2
Python maneja automáticamente StopIteration en los bucles for. El protocolo iterator permite usar tu clase con list comprehensions, funciones como list(), sum(), etc.
PATTERN_09

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

INVARIANTE
Partes del algoritmo que no cambian
VARIANTE
Partes que cada subclase personaliza
HOOK METHODS
Puntos de extensión opcionales
Estructura del Template
template_method()
step_1() [invariant]
step_2() [variant]
step_3() [invariant]

Generador de Banners

template.py
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)
python template.py
>>> Dots Style:
==================================================
HAPPY.CODING
==================================================

>>> Admire Style:
==================================================
!!! HAPPY CODING !!!
==================================================

>>> Cow Style:
==================================================
  ____________
 ( Happy Coding )
  ------------
   \   \_\_    _/_/
...
==================================================
Requiere instalar: 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.

Basado en "Mastering Python Design Patterns" // Kamon Ayeva & Sakis Kasampalis