
Patrones de Diseño Creacionales
Aprende patrones de diseño creacionales como Singleton, Factory, Builder y Prototype con ejemplos prácticos en Python.
¡Domina los patrones de diseño creacionales! En este tutorial especializado te guiaré paso a paso para que aprendas los patrones fundamentales de creación de objetos, incluyendo Singleton, Factory Method, Builder y Prototype, con ejemplos prácticos y casos de uso reales en Python.
Objetivo: Aprender los patrones de diseño creacionales más importantes, sus implementaciones en Python, ventajas, desventajas y cuándo aplicarlos en proyectos reales.
Índice
- Paso 1: ¿Qué son los patrones creacionales?
- Paso 2: Singleton - Una instancia única
- Paso 3: Factory Method - Creación flexible
- Paso 4: Abstract Factory - Familias de objetos
- Paso 5: Builder - Construcción paso a paso
- Paso 6: Prototype - Clonación de objetos
- Paso 7: Comparación y selección de patrones
- Paso 8: Proyecto práctico - Sistema de configuración
- Paso 9: Antipatrones y errores comunes
- Conclusión
- 💡 Tip Importante
Paso 1: ¿Qué son los patrones creacionales?
Los patrones de diseño creacionales se centran en el proceso de creación de objetos, proporcionando mecanismos flexibles para instanciar clases. Estos patrones abstraen el proceso de creación, haciendo que el sistema sea independiente de cómo se crean, componen y representan los objetos.
¿Por qué son importantes?
- Flexibilidad: Permiten cambiar las clases concretas sin modificar el código cliente
- Reutilización: Facilitan la creación de objetos complejos
- Mantenimiento: Separan la lógica de creación de la lógica de negocio
- Testing: Facilitan el uso de mocks y stubs
Clasificación de patrones creacionales
Patrón | Propósito | Complejidad |
---|---|---|
Singleton | Una instancia única global | Baja |
Factory Method | Delegar creación a subclases | Media |
Abstract Factory | Crear familias de objetos | Alta |
Builder | Construir objetos complejos paso a paso | Media |
Prototype | Clonar objetos existentes | Baja |
Paso 2: Singleton - Una instancia única
El patrón Singleton asegura que una clase tenga solo una instancia y proporciona un punto de acceso global a ella. Es útil para recursos compartidos como conexiones a base de datos, configuraciones globales o gestores de logs.
Implementación básica
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
# Solo se ejecuta una vez
if not hasattr(self, '_initialized'):
self._initialized = True
self.data = "Datos del singleton"
# Uso
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True - misma instancia
print(s1.data) # "Datos del singleton"
print(s2.data) # "Datos del singleton"
s1.data = "Modificado"
print(s2.data) # "Modificado" - comparten el estado
Singleton thread-safe
import threading
class SingletonThreadSafe:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None: # Double-check locking
cls._instance = super().__new__(cls)
return cls._instance
# Uso en entorno multi-hilo
def worker():
singleton = SingletonThreadSafe()
print(f"Instancia: {id(singleton)}")
threads = []
for i in range(5):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
for t in threads:
t.join()
Casos de uso comunes
- Configuración de aplicación:
AppConfig
- Conexión a base de datos:
DatabaseConnection
- Sistema de logging:
Logger
- Gestor de caché:
CacheManager
class DatabaseConnection(SingletonThreadSafe):
def __init__(self):
if not hasattr(self, '_initialized'):
self._initialized = True
self.connection_string = "postgresql://user:pass@localhost/db"
self._connect()
def _connect(self):
print(f"Conectando a: {self.connection_string}")
def query(self, sql):
return f"Ejecutando: {sql}"
# Uso
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True
print(db1.query("SELECT * FROM users"))
print(db2.query("SELECT * FROM products"))
Paso 3: Factory Method - Creación flexible
El patrón Factory Method define una interfaz para crear objetos, pero permite a las subclases decidir qué clase instanciar. Proporciona flexibilidad en la creación de objetos sin acoplar el código cliente a clases concretas.
Implementación básica
from abc import ABC, abstractmethod
class Creador(ABC):
@abstractmethod
def crear_producto(self):
pass
def operacion(self):
producto = self.crear_producto()
return f"Creador: {producto.operacion()}"
class Producto(ABC):
@abstractmethod
def operacion(self):
pass
# Productos concretos
class ProductoConcretoA(Producto):
def operacion(self):
return "Resultado del Producto A"
class ProductoConcretoB(Producto):
def operacion(self):
return "Resultado del Producto B"
# Creadores concretos
class CreadorConcretoA(Creador):
def crear_producto(self):
return ProductoConcretoA()
class CreadorConcretoB(Creador):
def crear_producto(self):
return ProductoConcretoB()
# Uso
def cliente(codigo_creador):
creadores = {
'A': CreadorConcretoA(),
'B': CreadorConcretoB()
}
creador = creadores.get(codigo_creador)
if creador:
print(creador.operacion())
else:
print("Creador no encontrado")
cliente('A') # "Creador: Resultado del Producto A"
cliente('B') # "Creador: Resultado del Producto B"
Factory Method con parámetros
class Transporte(ABC):
@abstractmethod
def entregar(self):
pass
class Camion(Transporte):
def entregar(self):
return "Entregando por tierra en camión"
class Barco(Transporte):
def entregar(self):
return "Entregando por mar en barco"
class Avion(Transporte):
def entregar(self):
return "Entregando por aire en avión"
class Logistica(ABC):
@abstractmethod
def crear_transporte(self) -> Transporte:
pass
def planificar_entrega(self):
transporte = self.crear_transporte()
return f"Logística: {transporte.entregar()}"
class LogisticaTerrestre(Logistica):
def crear_transporte(self):
return Camion()
class LogisticaMaritima(Logistica):
def crear_transporte(self):
return Barco()
class LogisticaAerea(Logistica):
def crear_transporte(self):
return Avion()
# Uso
def cliente_logistica(tipo_logistica):
logisticas = {
'terrestre': LogisticaTerrestre(),
'maritima': LogisticaMaritima(),
'aerea': LogisticaAerea()
}
logistica = logisticas.get(tipo_logistica)
if logistica:
print(logistica.planificar_entrega())
cliente_logistica('terrestre')
cliente_logistica('aerea')
Paso 4: Abstract Factory - Familias de objetos
El patrón Abstract Factory proporciona una interfaz para crear familias de objetos relacionados sin especificar sus clases concretas. Es útil cuando necesitas crear objetos que pertenecen a una misma familia o tema.
Implementación del patrón
from abc import ABC, abstractmethod
# Interfaces de productos
class Silla(ABC):
@abstractmethod
def sentarse(self):
pass
class Mesa(ABC):
@abstractmethod
def usar(self):
pass
class Sofa(ABC):
@abstractmethod
def descansar(self):
pass
# Productos concretos - Estilo Moderno
class SillaModerna(Silla):
def sentarse(self):
return "Sentándose en silla moderna"
class MesaModerna(Mesa):
def usar(self):
return "Usando mesa moderna"
class SofaModerno(Sofa):
def descansar(self):
return "Descansando en sofá moderno"
# Productos concretos - Estilo Victoriano
class SillaVictoriana(Silla):
def sentarse(self):
return "Sentándose en silla victoriana"
class MesaVictoriana(Mesa):
def usar(self):
return "Usando mesa victoriana"
class SofaVictoriano(Sofa):
def descansar(self):
return "Descansando en sofá victoriano"
# Abstract Factory
class FabricaMuebles(ABC):
@abstractmethod
def crear_silla(self) -> Silla:
pass
@abstractmethod
def crear_mesa(self) -> Mesa:
pass
@abstractmethod
def crear_sofa(self) -> Sofa:
pass
# Concrete Factories
class FabricaModerna(FabricaMuebles):
def crear_silla(self):
return SillaModerna()
def crear_mesa(self):
return MesaModerna()
def crear_sofa(self):
return SofaModerno()
class FabricaVictoriana(FabricaMuebles):
def crear_silla(self):
return SillaVictoriana()
def crear_mesa(self):
return MesaVictoriana()
def crear_sofa(self):
return SofaVictoriano()
# Cliente
def cliente_fabrica(fabrica: FabricaMuebles):
silla = fabrica.crear_silla()
mesa = fabrica.crear_mesa()
sofa = fabrica.crear_sofa()
print("Conjunto creado:")
print(f"- {silla.sentarse()}")
print(f"- {mesa.usar()}")
print(f"- {sofa.descansar()}")
# Uso
print("=== Estilo Moderno ===")
cliente_fabrica(FabricaModerna())
print("\n=== Estilo Victoriano ===")
cliente_fabrica(FabricaVictoriana())
Paso 5: Builder - Construcción paso a paso
El patrón Builder permite construir objetos complejos paso a paso. Separa la construcción de un objeto de su representación, permitiendo el mismo proceso de construcción para crear diferentes representaciones.
Implementación básica
from abc import ABC, abstractmethod
class Constructor(ABC):
@abstractmethod
def construir_motor(self):
pass
@abstractmethod
def construir_carrocera(self):
pass
@abstractmethod
def construir_llantas(self):
pass
@abstractmethod
def obtener_vehiculo(self):
pass
class Vehiculo:
def __init__(self):
self.partes = []
def agregar_parte(self, parte):
self.partes.append(parte)
def mostrar_partes(self):
return f"Vehículo con: {', '.join(self.partes)}"
class ConstructorAutoDeportivo(Constructor):
def __init__(self):
self.vehiculo = Vehiculo()
def construir_motor(self):
self.vehiculo.agregar_parte("Motor V8 deportivo")
def construir_carrocera(self):
self.vehiculo.agregar_parte("Carrocería aerodinámica")
def construir_llantas(self):
self.vehiculo.agregar_parte("Llantas de alto rendimiento")
def obtener_vehiculo(self):
return self.vehiculo
class ConstructorAutoFamiliar(Constructor):
def __init__(self):
self.vehiculo = Vehiculo()
def construir_motor(self):
self.vehiculo.agregar_parte("Motor V4 eficiente")
def construir_carrocera(self):
self.vehiculo.agregar_parte("Carrocería espaciosa")
def construir_llantas(self):
self.vehiculo.agregar_parte("Llantas todo terreno")
def obtener_vehiculo(self):
return self.vehiculo
class Director:
def __init__(self, constructor):
self.constructor = constructor
def construir_vehiculo_completo(self):
self.constructor.construir_motor()
self.constructor.construir_carrocera()
self.constructor.construir_llantas()
return self.constructor.obtener_vehiculo()
# Uso
director_deportivo = Director(ConstructorAutoDeportivo())
auto_deportivo = director_deportivo.construir_vehiculo_completo()
print(auto_deportivo.mostrar_partes())
director_familiar = Director(ConstructorAutoFamiliar())
auto_familiar = director_familiar.construir_vehiculo_completo()
print(auto_familiar.mostrar_partes())
Builder fluido (fluent interface)
class ConstructorPizza:
def __init__(self):
self.masa = None
self.salsa = None
self.queso = None
self.ingredientes = []
def con_masa(self, masa):
self.masa = masa
return self
def con_salsa(self, salsa):
self.salsa = salsa
return self
def con_queso(self, queso):
self.queso = queso
return self
def agregar_ingrediente(self, ingrediente):
self.ingredientes.append(ingrediente)
return self
def construir(self):
return Pizza(self.masa, self.salsa, self.queso, self.ingredientes)
class Pizza:
def __init__(self, masa, salsa, queso, ingredientes):
self.masa = masa
self.salsa = salsa
self.queso = queso
self.ingredientes = ingredientes
def __str__(self):
return f"Pizza: {self.masa} masa, salsa {self.salsa}, queso {self.queso}, ingredientes: {', '.join(self.ingredientes)}"
# Uso fluido
pizza = (ConstructorPizza()
.con_masa("delgada")
.con_salsa("tomate")
.con_queso("mozzarella")
.agregar_ingrediente("pepperoni")
.agregar_ingrediente("champiñones")
.construir())
print(pizza)
Paso 6: Prototype - Clonación de objetos
El patrón Prototype permite copiar objetos existentes sin depender de sus clases concretas. Es útil cuando la creación de un objeto es costosa o compleja, y puedes reutilizar instancias existentes como prototipos.
Implementación básica
from abc import ABC, abstractmethod
import copy
class Prototipo(ABC):
@abstractmethod
def clonar(self):
pass
class Documento(Prototipo):
def __init__(self, titulo, contenido, autor):
self.titulo = titulo
self.contenido = contenido
self.autor = autor
self.fecha_creacion = "2025-01-01"
def clonar(self):
# Clonación superficial
return copy.copy(self)
def clonar_profunda(self):
# Clonación profunda
return copy.deepcopy(self)
def __str__(self):
return f"Documento: {self.titulo} por {self.autor}"
# Uso
documento_original = Documento("Tutorial Python", "Contenido del tutorial", "José")
# Clonación superficial
documento_clonado = documento_original.clonar()
documento_clonado.titulo = "Tutorial Avanzado Python"
print(documento_original)
print(documento_clonado)
# Clonación profunda con objetos anidados
class DocumentoComplejo(Prototipo):
def __init__(self, titulo, metadata):
self.titulo = titulo
self.metadata = metadata # Diccionario anidado
def clonar(self):
return copy.deepcopy(self)
def __str__(self):
return f"Documento: {self.titulo}, Metadata: {self.metadata}"
metadata = {"autor": "José", "version": 1.0, "tags": ["python", "tutorial"]}
doc_complejo = DocumentoComplejo("Tutorial Complejo", metadata)
doc_clonado = doc_complejo.clonar()
doc_clonado.metadata["version"] = 2.0
doc_clonado.metadata["tags"].append("avanzado")
print(doc_complejo)
print(doc_clonado)
Registro de prototipos
class RegistroPrototipos:
def __init__(self):
self.prototipos = {}
def agregar_prototipo(self, nombre, prototipo):
self.prototipos[nombre] = prototipo
def obtener_prototipo(self, nombre):
prototipo = self.prototipos.get(nombre)
if prototipo:
return prototipo.clonar()
raise ValueError(f"Prototipo '{nombre}' no encontrado")
# Configuración del registro
registro = RegistroPrototipos()
# Prototipos base
documento_base = Documento("Documento Base", "Contenido base", "Autor Base")
email_base = Documento("Email Base", "Estimado cliente...", "Sistema")
registro.agregar_prototipo("documento", documento_base)
registro.agregar_prototipo("email", email_base)
# Crear instancias desde prototipos
doc1 = registro.obtener_prototipo("documento")
doc1.titulo = "Mi Documento Personal"
email1 = registro.obtener_prototipo("email")
email1.contenido = "Estimado cliente, su pedido ha sido procesado..."
print(doc1)
print(email1)
Paso 7: Comparación y selección de patrones
Cuándo usar cada patrón
Patrón | Cuándo usarlo | Ventajas | Desventajas |
---|---|---|---|
Singleton | Una instancia global, recursos compartidos | Garantiza unicidad, acceso global | Acoplamiento fuerte, difícil de testear |
Factory Method | Creación flexible, subclases deciden | Extensible, desacopla cliente | Puede ser overkill para casos simples |
Abstract Factory | Familias de objetos relacionados | Consistencia, extensible | Complejo de implementar |
Builder | Objetos complejos, construcción paso a paso | Flexible, legible | Verbose para objetos simples |
Prototype | Creación costosa, muchos objetos similares | Eficiente, flexible | Requiere implementación de clonación |
Antipatrones a evitar
- Singleton overuse: No todo necesita ser singleton
- Factory explosion: No crear factories para todo
- Builder for simple objects: Over-engineering
Paso 8: Proyecto práctico - Sistema de configuración
Vamos a crear un sistema de configuración que utilice múltiples patrones creacionales para demostrar su uso en un escenario real.
import json
import copy
from abc import ABC, abstractmethod
# Singleton para configuración global
class ConfiguracionGlobal:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, '_initialized'):
self._initialized = True
self.config = {}
self._cargar_configuracion_default()
def _cargar_configuracion_default(self):
self.config = {
"database": {
"host": "localhost",
"port": 5432,
"name": "app_db"
},
"logging": {
"level": "INFO",
"file": "app.log"
},
"features": {
"cache": True,
"notifications": False
}
}
def obtener_config(self, seccion=None):
if seccion:
return self.config.get(seccion, {})
return self.config
def actualizar_config(self, seccion, clave, valor):
if seccion not in self.config:
self.config[seccion] = {}
self.config[seccion][clave] = valor
# Prototype para perfiles de configuración
class PerfilConfiguracion:
def __init__(self, nombre, config_base):
self.nombre = nombre
self.config = copy.deepcopy(config_base)
def clonar(self):
return copy.deepcopy(self)
def personalizar(self, **kwargs):
for clave, valor in kwargs.items():
if '.' in clave:
seccion, subclave = clave.split('.', 1)
if seccion not in self.config:
self.config[seccion] = {}
self.config[seccion][subclave] = valor
else:
self.config[clave] = valor
def __str__(self):
return f"Perfil '{self.nombre}': {json.dumps(self.config, indent=2)}"
# Factory para crear perfiles
class FabricaPerfiles:
def __init__(self):
self.config_global = ConfiguracionGlobal()
def crear_perfil_desarrollo(self):
perfil = PerfilConfiguracion("desarrollo", self.config_global.obtener_config())
perfil.personalizar(
database__host="dev-db.local",
database__name="dev_app_db",
logging__level="DEBUG",
features__cache=False
)
return perfil
def crear_perfil_produccion(self):
perfil = PerfilConfiguracion("produccion", self.config_global.obtener_config())
perfil.personalizar(
database__host="prod-db.company.com",
database__port=5433,
logging__level="WARNING",
features__notifications=True
)
return perfil
def crear_perfil_testing(self):
perfil = PerfilConfiguracion("testing", self.config_global.obtener_config())
perfil.personalizar(
database__host="test-db.local",
database__name="test_app_db",
logging__level="DEBUG",
features__cache=False
)
return perfil
# Builder para configuración personalizada
class ConstructorConfiguracionPersonalizada:
def __init__(self):
self.config_global = ConfiguracionGlobal()
self.perfil = PerfilConfiguracion("personalizado", self.config_global.obtener_config())
def configurar_base_datos(self, host, port=5432, name=None):
self.perfil.config["database"].update({
"host": host,
"port": port
})
if name:
self.perfil.config["database"]["name"] = name
return self
def configurar_logging(self, level="INFO", file=None):
self.perfil.config["logging"]["level"] = level
if file:
self.perfil.config["logging"]["file"] = file
return self
def habilitar_features(self, *features):
for feature in features:
self.perfil.config["features"][feature] = True
return self
def deshabilitar_features(self, *features):
for feature in features:
self.perfil.config["features"][feature] = False
return self
def construir(self):
return self.perfil
# Uso del sistema completo
def main():
# Singleton - configuración global
config_global = ConfiguracionGlobal()
print("Configuración global:")
print(json.dumps(config_global.obtener_config(), indent=2))
# Factory - crear perfiles predefinidos
fabrica = FabricaPerfiles()
perfil_dev = fabrica.crear_perfil_desarrollo()
perfil_prod = fabrica.crear_perfil_produccion()
print(f"\n{perfil_dev}")
print(f"\n{perfil_prod}")
# Builder - configuración personalizada
constructor = ConstructorConfiguracionPersonalizada()
perfil_personalizado = (constructor
.configurar_base_datos("mi-servidor.com", port=3306, name="mi_db")
.configurar_logging(level="ERROR", file="errores.log")
.habilitar_features("cache", "notifications")
.deshabilitar_features("debug")
.construir())
print(f"\n{perfil_personalizado}")
# Prototype - clonar y modificar perfiles
perfil_clonado = perfil_dev.clonar()
perfil_clonado.nombre = "desarrollo-modificado"
perfil_clonado.personalizar(database__port=3307)
print(f"\n{perfil_clonado}")
if __name__ == "__main__":
main()
Paso 9: Antipatrones y errores comunes
Errores comunes en patrones creacionales
- Singletonitis: Usar Singleton para todo
- Factory explosion: Crear factories innecesarias
- Builder overkill: Usar Builder para objetos simples
- Prototype misuse: Clonar cuando no es necesario
Cómo evitarlos
# ❌ Antipatrón: Singleton para todo
class TodoEsSingleton:
_instances = {}
def __new__(cls):
if cls not in cls._instances:
cls._instances[cls] = super().__new__(cls)
return cls._instances[cls]
# ✅ Mejor: Usar inyección de dependencias
class ServicioUsuario:
def __init__(self, repositorio):
self.repositorio = repositorio
class RepositorioUsuario:
def __init__(self, db_connection):
self.db = db_connection
# Configuración centralizada
def configurar_aplicacion():
db = DatabaseConnection()
repo = RepositorioUsuario(db)
servicio = ServicioUsuario(repo)
return servicio
Conclusión
¡Has dominado los patrones de diseño creacionales! Estos patrones te permiten crear objetos de manera flexible y mantenible, separando la lógica de creación de la lógica de negocio.
Practica implementando estos patrones en tus proyectos y combina diferentes patrones según las necesidades específicas de tu aplicación.
Para más tutoriales sobre patrones de diseño, visita nuestra sección de tutoriales.
¡Sigue practicando y aplicando estos patrones en proyectos reales!
💡 Tip Importante
📝 Mejores Prácticas en Patrones Creacionales
- Elige el patrón adecuado: Analiza el problema antes de seleccionar un patrón
- Principio de responsabilidad única: Cada clase debe tener una sola razón para cambiar
- Principio abierto/cerrado: Los patrones deben estar abiertos a extensión pero cerrados a modificación
- Composición sobre herencia: Prefiere composición cuando sea posible
- Testing: Los patrones deben facilitar el testing unitario
- Documentación: Documenta por qué elegiste un patrón específico
- Simplicidad: No uses patrones complejos para problemas simples
- Consistencia: Mantén consistencia en el uso de patrones en tu proyecto
📚 Recursos Recomendados:
- Design Patterns: Elements of Reusable Object-Oriented Software - Gang of Four
- Head First Design Patterns - Libro práctico
- Refactoring.Guru - Creational Patterns - Ejemplos interactivos
¡Estos patrones te ayudarán a crear software más flexible y mantenible!
No hay comentarios aún
Sé el primero en comentar este tutorial.