Patrones Factory y Builder
Las fábricas centralizan la creación de objetos detrás de una función o método de clase para que los llamadores permanezcan desacoplados de los tipos concretos. Los constructores (Builders) ensamblan objetos complejos paso a paso cuando los constructores necesitarían demasiados parámetros o combinaciones opcionales.
Receta
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
from dataclasses import dataclass
@dataclass
class HttpClient:
base_url: str
timeout: float = 30.0
retries: int = 3
def http_client_from_env(env: dict) -> HttpClient:
return HttpClient(
base_url=env["API_URL"],
timeout=float(env.get("HTTP_TIMEOUT", 30)),
)Cuándo usar esto:
- Factory cuando la lógica de creación se ramifica según la configuración, el entorno o el tipo en tiempo de ejecución.
- Builder cuando los objetos tienen muchos campos opcionales o validación entre pasos.
- Constructores de classmethod (
from_dict,from_url) como fábricas ligeras. - Ocultar ciclos de importación - el módulo de fábrica importa clases concretas, los llamadores no.
- Dobles de prueba - las fábricas aceptan backends inyectables.
Ejemplo de Trabajo
Una fábrica selecciona backends de almacenamiento; un constructor crea una configuración de informe validada.
from dataclasses import dataclass, field
from typing import Protocol
class Storage(Protocol):
def save(self, key: str, data: bytes) -> None: ...
@dataclass
class MemoryStorage:
_data: dict[str, bytes] = field(default_factory=dict)
def save(self, key: str, data: bytes) -> None:
self._data[key] = data
@dataclass
class FileStorage:
root: str
def save(self, key: str, data: bytes) -> None:
print(f"escribir {key} en {self.root}")
def storage_factory(kind: str, **kwargs) -> Storage:
if kind == "memory":
return MemoryStorage()
if kind == "file":
return FileStorage(root=kwargs["root"])
raise ValueError(f"almacenamiento desconocido: {kind}")
@dataclass
class ReportConfig:
title: str
columns: list[str]
page_size: int = 50
class ReportConfigBuilder:
def __init__(self, title: str) -> None:
self._title = title
self._columns: list[str] = []
self._page_size = 50
def add_column(self, name: str) -> "ReportConfigBuilder":
self._columns.append(name)
return self
def page_size(self, size: int) -> "ReportConfigBuilder":
if size < 1:
raise ValueError("page_size debe ser positivo")
self._page_size = size
return self
def build(self) -> ReportConfig:
if not self._columns:
raise ValueError("se requiere al menos una columna")
return ReportConfig(self._title, list(self._columns), self._page_size)
store = storage_factory("memory")
store.save("report.pdf", b"%PDF")
cfg = (
ReportConfigBuilder("Ventas")
.add_column("sku")
.add_column("ingresos")
.page_size(100)
.build()
)
print(cfg)Lo que esto demuestra:
- La fábrica encapsula la construcción ramificada.
- Los tipos de protocolo mantienen a los llamadores independientes del almacenamiento.
- Los métodos del constructor devuelven
selfpara encadenamiento fluido. build()valida invariantes antes de devolver el producto.
Análisis Profundo
Cómo Funciona
- Fábrica: una función mapea entradas a una instancia concreta.
- Constructor: acumula estado, valida en
build(), evita constructores telescópicos. @dataclass+field(default_factory=...)a menudo reemplaza a los constructores para casos simples.- Los métodos de clase (
from_env,parse) son fábricas idiomáticas de Python. - La inyección de dependencias pasa fábricas en lugar de instancias para la vinculación tardía.
Fábrica vs. Constructor
| Patrón | Resuelve | Idioma de Python |
|---|---|---|
| Fábrica | Qué clase instanciar | función o @classmethod |
| Constructor | Muchos pasos de ensamblaje opcionales | constructor fluido o dataclass anidado |
Notas de Python
@dataclass
class Settings:
debug: bool = False
@classmethod
def from_env(cls, env: dict) -> "Settings":
return cls(debug=env.get("DEBUG") == "1")Trampas Comunes
- Fábrica Dios (God factory) - una función con 20 ramas
elif. Solución: diccionario de registro o fábricas de subclases por dominio. - Constructor sin validación - escapa estado parcial inválido. Solución: validar solo en
build(), mantener el constructor mutable internamente. - Sobrediseño de dataclasses simples - tres campos opcionales no necesitan un constructor. Solución: usar valores predeterminados y
dataclass. - Fábrica ocultando efectos secundarios - crear un cliente no debería conectarse a la red. Solución: separar
createdeconnect. - Reutilización de constructor mutable - reutilizar el mismo constructor después de
build()causa interferencias. Solución: documentar uso único o clonar enbuild().
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
Valores predeterminados de @dataclass | Pocos campos opcionales | Validación compleja entre campos |
| Modelos Pydantic | Validación + análisis desde entorno/JSON | Scripts sin dependencias |
Constructor **kwargs | Solo biblioteca interna | API pública necesita firmas estables |
| Fábrica abstracta OOP | Familias de productos relacionados | Línea de productos única |
Preguntas Frecuentes
¿Necesito una clase Builder en Python?
A menudo no. dataclass con valores predeterminados, model_validate o una única función de fábrica cubren muchos casos. Los constructores ayudan cuando el ensamblaje es de varios pasos y el orden importa.
¿Dónde deben vivir las fábricas?
En el módulo que posee los tipos de producto, o un factories.py dedicado cuando la creación cruza subpaquetes.
¿Cómo ayudan las fábricas a las pruebas?
Pase una fábrica invocable que devuelva falsificaciones/mocks. La producción usa la fábrica real; las pruebas inyectan lambda: FakeStorage().
¿Son los classmethods fábricas?
Sí. from_dict, from_url y parse son métodos de fábrica estándar de Python.
¿Pueden los builders ser dataclasses?
Use una clase constructora mutable o dataclass con frozen=False internamente; congele el producto con frozen=True.
¿Qué pasa con __init_subclass__ para el registro?
Los frameworks de plugins registran subclases automáticamente, una forma de descubrimiento de fábricas.
¿Cómo encaja Pydantic?
Model.model_validate(data) es una fábrica validada a partir de diccionarios/JSON; prefiera esto para objetos de configuración.
¿Debería build() devolver una copia?
Si el producto es mutable, devolver una copia profunda evita que los llamadores modifiquen el estado del constructor en caché.
¿Fábrica vs. contenedor de inyección de dependencias?
Los contenedores de DI llaman a fábricas/proveedores. Las aplicaciones simples usan funciones normales sin un framework.
¿Cómo se tipifica una fábrica?
Callable[[], Storage] o Protocol con un método create(). Genéricos cuando el tipo de producto varía.
Relacionado
- Inyección de Dependencias en Python - conexión de fábricas
- Patrones Strategy y Dispatch - fábrica + strategy
- Null Object y Sentinels - instancias de producto predeterminadas
- Conceptos Básicos de Patrones Pythonicos - resumen de la sección
Versiones de la pila: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.