miércoles, 14 de enero de 2026

Principios Fundamentales de Diseño en Python - Parte 3 - Diseño Creacional

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

Patrones de Diseño Creacionales

5 patrones esenciales para controlar la creación de objetos en Python

Los patrones creacionales son la primera categoría de los 23 patrones de diseño del Gang of Four. Estos patrones manejan diferentes aspectos de la creación de objetos, proporcionando alternativas elegantes cuando la instanciación directa mediante __init__() no es conveniente.

En este artículo exploraremos cinco patrones fundamentales: Factory Method, Abstract Factory, Builder, Prototype y Singleton. Cada uno resuelve problemas específicos de creación de objetos.

PATTERN_01

Factory Method

El patrón Factory Method simplifica la creación de objetos a través de una función centralizada. El cliente solicita un objeto sin conocer de qué clase proviene, reduciendo el acoplamiento entre el código que crea objetos y el que los utiliza.

Beneficios

DESACOPLAMIENTO
Separa la creación del uso del objeto
CENTRALIZACIÓN
Un solo punto para rastrear objetos creados
FLEXIBILIDAD
Fácil añadir nuevos tipos sin modificar código existente
Flujo del Factory Method
Cliente
—→
Factory()
—→
Objeto A
Objeto B

Ejemplo: Sistema de Serialización

Creamos una fábrica que retorna el serializador adecuado según el formato solicitado:

factory_method.py
import json
import xml.etree.ElementTree as ET

class JSONSerializer:
    def serialize(self, data: dict) -> str:
        return json.dumps(data, indent=2)

class XMLSerializer:
    def serialize(self, data: dict) -> str:
        root = ET.Element("data")
        for key, value in data.items():
            child = ET.SubElement(root, key)
            child.text = str(value)
        return ET.tostring(root, encoding="unicode")

# Factory Method
def serializer_factory(format_type: str):
    """Retorna el serializador según el formato."""
    serializers = {
        "json": JSONSerializer,
        "xml": XMLSerializer,
    }
    serializer_class = serializers.get(format_type.lower())
    if not serializer_class:
        raise ValueError(f"Formato desconocido: {format_type}")
    return serializer_class()

# Uso
if __name__ == "__main__":
    data = {"nombre": "Claude", "version": "3.5"}
    
    json_serializer = serializer_factory("json")
    print(json_serializer.serialize(data))
    
    xml_serializer = serializer_factory("xml")
    print(xml_serializer.serialize(data))
python factory_method.py
{
  "nombre": "Claude",
  "version": "3.5"
}
<data><nombre>Claude</nombre><version>3.5</version></data>
El cliente no necesita conocer las clases JSONSerializer o XMLSerializer. Solo interactúa con la función factory, que decide qué instanciar.
PATTERN_02

Abstract Factory

El patrón Abstract Factory es una extensión del Factory Method. Proporciona una interfaz para crear familias de objetos relacionados sin especificar sus clases concretas. Es ideal cuando un sistema debe ser independiente de cómo se crean sus productos.

Familias de Objetos
GameFactory
FrogWorld
WizardWorld

Ejemplo: Juego de Mundos

Cada mundo tiene su propio héroe y enemigo. La fábrica abstracta crea la familia completa:

abstract_factory.py
# Familia 1: FrogWorld
class Frog:
    def __init__(self, name: str):
        self.name = name
    
    def interact_with(self, obstacle):
        print(f"{self.name} the Frog encounters a {obstacle} and eats it!")

class Bug:
    def __str__(self):
        return "bug"

class FrogWorld:
    def __init__(self, name: str):
        print("------ Frog World -------")
        self.player_name = name
    
    def make_character(self):
        return Frog(self.player_name)
    
    def make_obstacle(self):
        return Bug()

# Familia 2: WizardWorld
class Wizard:
    def __init__(self, name: str):
        self.name = name
    
    def interact_with(self, obstacle):
        print(f"{self.name} the Wizard battles against {obstacle} and defeats it!")

class Ork:
    def __str__(self):
        return "an evil ork"

class WizardWorld:
    def __init__(self, name: str):
        print("------ Wizard World -------")
        self.player_name = name
    
    def make_character(self):
        return Wizard(self.player_name)
    
    def make_obstacle(self):
        return Ork()

# Cliente que usa la fábrica abstracta
class GameEnvironment:
    def __init__(self, factory):
        self.hero = factory.make_character()
        self.obstacle = factory.make_obstacle()
    
    def play(self):
        self.hero.interact_with(self.obstacle)

# Uso
if __name__ == "__main__":
    age = 15
    name = "Arthur"
    
    game = FrogWorld if age < 18 else WizardWorld
    environment = GameEnvironment(game(name))
    environment.play()
python abstract_factory.py
------ Frog World -------
Arthur the Frog encounters a bug and eats it!
GameEnvironment no conoce las clases concretas. Solo sabe que la fábrica proporciona make_character() y make_obstacle().
PATTERN_03

Builder

El patrón Builder separa la construcción de un objeto complejo de su representación. Permite crear objetos paso a paso, donde el objeto no está completo hasta que todas sus partes están listas. La misma construcción puede crear diferentes representaciones.

Builder vs Factory

Factory
  • Creación en un solo paso
  • Objetos simples o similares
  • Retorna el objeto inmediatamente
Builder
  • Construcción paso a paso
  • Objetos complejos con múltiples partes
  • El objeto se entrega al final
Flujo del Builder
Director
—→
Builder
—→
Producto

Ejemplo: Pizzería

Un mesero (Director) usa builders específicos para construir pizzas paso a paso:

builder.py
from enum import Enum
import time

class PizzaProgress(Enum):
    queued = 1
    preparation = 2
    baking = 3
    ready = 4

class PizzaDough(Enum):
    thin = "thin"
    thick = "thick"

STEP_DELAY = 1  # segundos

class Pizza:
    def __init__(self, name: str):
        self.name = name
        self.dough = None
        self.sauce = None
        self.topping = []
    
    def __str__(self):
        return self.name
    
    def prepare_dough(self, dough):
        self.dough = dough
        print(f"Preparing {dough.value} dough...")
        time.sleep(STEP_DELAY)

# Builder concreto: Margarita
class MargaritaBuilder:
    def __init__(self):
        self.pizza = Pizza("Margarita")
        self.progress = PizzaProgress.queued
    
    def prepare_dough(self):
        self.progress = PizzaProgress.preparation
        self.pizza.prepare_dough(PizzaDough.thin)
    
    def add_sauce(self):
        print("Adding tomato sauce...")
        self.pizza.sauce = "tomato"
        time.sleep(STEP_DELAY)
    
    def add_topping(self):
        topping = "mozzarella, oregano"
        print(f"Adding topping: {topping}")
        self.pizza.topping.append(topping)
        time.sleep(STEP_DELAY)
    
    def bake(self):
        self.progress = PizzaProgress.baking
        print("Baking for 5 seconds...")
        time.sleep(5)
        self.progress = PizzaProgress.ready
        print("Pizza is ready!")

# Director: Mesero
class Waiter:
    def __init__(self):
        self.builder = None
    
    def construct_pizza(self, builder):
        self.builder = builder
        steps = [
            builder.prepare_dough,
            builder.add_sauce,
            builder.add_topping,
            builder.bake,
        ]
        for step in steps:
            step()
    
    @property
    def pizza(self):
        return self.builder.pizza

# Uso
if __name__ == "__main__":
    waiter = Waiter()
    waiter.construct_pizza(MargaritaBuilder())
    print(f"\nEnjoy your {waiter.pizza}!")
python builder.py
Preparing thin dough...
Adding tomato sauce...
Adding topping: mozzarella, oregano
Baking for 5 seconds...
Pizza is ready!

Enjoy your Margarita!
El Waiter no conoce los detalles de cada pizza. Solo ejecuta los pasos en orden. Crear una nueva pizza (ej: CreamyBacon) solo requiere un nuevo Builder.
PATTERN_04

Prototype

El patrón Prototype permite crear nuevos objetos clonando instancias existentes en lugar de crearlas desde cero. Es útil cuando el costo de inicializar un objeto es mayor que copiarlo, o cuando necesitas duplicar un objeto con referencias complejas.

Casos de Uso

Duplicar objetos con datos de BD
Crear variantes de un objeto base
Preservar estado antes de modificaciones
Evitar constructores costosos
Clonación
Objeto Original
— clone() —→
Copia Independiente

Ejemplo: Website con Variantes

Clonamos un sitio web base para crear variantes específicas:

prototype.py
import copy

class Website:
    def __init__(self, name: str, domain: str, description: str):
        self.name = name
        self.domain = domain
        self.description = description
        self.pages = ["Home", "About", "Contact"]  # Lista mutable
    
    def clone(self):
        """Crea una copia profunda del objeto."""
        return copy.deepcopy(self)
    
    def __str__(self):
        return (f"Website: {self.name}\n"
                f"Domain: {self.domain}\n"
                f"Pages: {self.pages}")

# Uso
if __name__ == "__main__":
    # Sitio base (prototipo)
    base_site = Website(
        name="Corporate Template",
        domain="example.com",
        description="Base corporate website"
    )
    
    # Clonar para cliente A
    client_a = base_site.clone()
    client_a.name = "TechCorp"
    client_a.domain = "techcorp.com"
    client_a.pages.append("Products")  # Modificar sin afectar original
    
    # Clonar para cliente B
    client_b = base_site.clone()
    client_b.name = "DataInc"
    client_b.domain = "datainc.io"
    client_b.pages.append("Services")
    
    print("=== Original ===")
    print(base_site)
    print("\n=== Client A ===")
    print(client_a)
    print("\n=== Client B ===")
    print(client_b)
python prototype.py
=== Original ===
Website: Corporate Template
Domain: example.com
Pages: ['Home', 'About', 'Contact']

=== Client A ===
Website: TechCorp
Domain: techcorp.com
Pages: ['Home', 'About', 'Contact', 'Products']

=== Client B ===
Website: DataInc
Domain: datainc.io
Pages: ['Home', 'About', 'Contact', 'Services']
copy.deepcopy() crea copias independientes de objetos anidados. Sin ella, las listas pages serían compartidas entre todos los clones.
PATTERN_05

Singleton

El patrón Singleton garantiza que una clase tenga una única instancia y proporciona un punto de acceso global a ella. Es útil para recursos compartidos como conexiones a BD, configuraciones globales o sistemas de logging.

Casos de Uso

Pool de conexiones a base de datos
Gestores de configuración
Sistemas de logging
Caché de aplicación
Una Sola Instancia
Cliente 1
Cliente 2
Cliente 3
—→
Singleton Instance

Implementación con Metaclass

singleton.py
class SingletonMeta(type):
    """Metaclass que implementa el patrón Singleton."""
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]

class DatabaseConnection(metaclass=SingletonMeta):
    def __init__(self):
        self.connection_string = "postgresql://localhost:5432/mydb"
        print("Initializing database connection...")
    
    def query(self, sql: str):
        print(f"Executing: {sql}")

# Uso
if __name__ == "__main__":
    # Primera llamada: crea la instancia
    db1 = DatabaseConnection()
    print(f"db1 id: {id(db1)}")
    
    # Segunda llamada: retorna la misma instancia
    db2 = DatabaseConnection()
    print(f"db2 id: {id(db2)}")
    
    # Verificar que son el mismo objeto
    print(f"\nSame instance? {db1 is db2}")
    
    # Ambas referencias ejecutan en la misma conexión
    db1.query("SELECT * FROM users")
python singleton.py
Initializing database connection...
db1 id: 140234567890
db2 id: 140234567890

Same instance? True
Executing: SELECT * FROM users
El mensaje "Initializing..." solo aparece una vez. La metaclass intercepta cada llamada al constructor y retorna la instancia existente si ya fue creada.

Alternativa Pythonica: Módulo como Singleton

En Python, los módulos son singletons naturales. Puedes simplemente usar variables a nivel de módulo:

config.py
# config.py - Este módulo es un singleton natural
# Python solo lo importa una vez

_config = {
    "debug": False,
    "database": "postgresql://localhost/app",
    "cache_timeout": 300
}

def get(key: str):
    return _config.get(key)

def set(key: str, value):
    _config[key] = value

# En otro archivo:
# import config
# config.get("debug")  # Siempre el mismo estado
BONUS_PATTERN

Object Pool

El patrón Object Pool mantiene un conjunto de objetos reutilizables para evitar el costo de crear y destruir objetos frecuentemente. Es ideal para objetos costosos de inicializar, como conexiones de red o threads.

Pool de Objetos
Cliente
— acquire —→
Pool
← release ←
Cliente

Ejemplo: Pool de Autos

object_pool.py
class Car:
    def __init__(self, make: str, model: str):
        self.make = make
        self.model = model
        self.in_use = False
    
    def __str__(self):
        return f"{self.make} {self.model}"

class CarPool:
    def __init__(self):
        self._available = []
        self._in_use = []
    
    def acquire(self) -> Car:
        """Obtiene un auto del pool o crea uno nuevo."""
        if not self._available:
            # Solo crea si no hay disponibles
            print("Creating new car...")
            self._available.append(Car("BMW", "M3"))
        
        car = self._available.pop()
        car.in_use = True
        self._in_use.append(car)
        return car
    
    def release(self, car: Car):
        """Devuelve un auto al pool."""
        car.in_use = False
        self._in_use.remove(car)
        self._available.append(car)
    
    @property
    def stats(self) -> str:
        return f"Available: {len(self._available)}, In use: {len(self._in_use)}"

# Uso
if __name__ == "__main__":
    pool = CarPool()
    
    print("Acquiring car 1...")
    car1 = pool.acquire()
    print(f"Got: {car1} | {pool.stats}")
    
    print("\nReleasing car 1...")
    pool.release(car1)
    print(f"{pool.stats}")
    
    print("\nAcquiring car 2...")
    car2 = pool.acquire()  # Reutiliza el auto existente
    print(f"Got: {car2} | Same car? {car1 is car2}")
python object_pool.py
Acquiring car 1...
Creating new car...
Got: BMW M3 | Available: 0, In use: 1

Releasing car 1...
Available: 1, In use: 0

Acquiring car 2...
Got: BMW M3 | Same car? True
"Creating new car..." solo aparece una vez. La segunda adquisición reutiliza el objeto existente, evitando la sobrecarga de instanciación.

Resumen de Patrones

01 // Factory Method

Centraliza la creación de objetos en una función que decide qué instanciar según el parámetro recibido.

02 // Abstract Factory

Crea familias de objetos relacionados sin especificar sus clases concretas.

03 // Builder

Construye objetos complejos paso a paso, separando la construcción de la representación.

04 // Prototype

Crea objetos clonando instancias existentes en lugar de instanciarlos desde cero.

05 // Singleton

Garantiza una única instancia de una clase con un punto de acceso global.

06 // Object Pool

Mantiene objetos reutilizables para evitar el costo de creación y destrucción frecuente.

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