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.
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
Ejemplo: Sistema de Serialización
Creamos una fábrica que retorna el serializador adecuado según el formato solicitado:
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))
"nombre": "Claude",
"version": "3.5"
}
<data><nombre>Claude</nombre><version>3.5</version></data>
JSONSerializer o XMLSerializer. Solo interactúa con la función factory, que decide qué instanciar.
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.
Ejemplo: Juego de Mundos
Cada mundo tiene su propio héroe y enemigo. La fábrica abstracta crea la familia completa:
# 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()
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().
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
- Creación en un solo paso
- Objetos simples o similares
- Retorna el objeto inmediatamente
- Construcción paso a paso
- Objetos complejos con múltiples partes
- El objeto se entrega al final
Ejemplo: Pizzería
Un mesero (Director) usa builders específicos para construir pizzas paso a paso:
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}!")
Adding tomato sauce...
Adding topping: mozzarella, oregano
Baking for 5 seconds...
Pizza is ready!
Enjoy your Margarita!
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.
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
Ejemplo: Website con Variantes
Clonamos un sitio web base para crear variantes específicas:
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)
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.
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
Implementación con Metaclass
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")
db1 id: 140234567890
db2 id: 140234567890
Same instance? True
Executing: SELECT * FROM users
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 - 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
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.
Ejemplo: Pool de Autos
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}")
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
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.