Patrón Manager
Centraliza el ciclo de vida, búsqueda y limpieza de objetos detrás de una clase manager dedicada — desde pools de recursos hasta entity managers.
Descripción General
El Patrón Manager pone el ciclo de vida, la coordinación y el control de acceso de un conjunto de objetos relacionados detrás de una clase manager dedicada. En lugar de dejar que cada llamador cree, busque y destruya objetos por su cuenta, el manager es dueño de esas operaciones y hace cumplir las reglas que las rodean.
Este patrón aparece por todas partes una vez que sabes qué buscar, aunque rara vez lleva ese nombre. Los entity managers en ORMs, los pools de conexiones en código de red, los scene managers en motores de juegos, los registros de servicios en sistemas operativos — la misma idea, distinto dominio. Piensa en el manager como la única puerta de entrada a una colección de objetos gestionados.
Sin uno, los fallos son predecibles: los objetos se crean en tres lugares distintos con configuraciones ligeramente diferentes, los índices se desincronizan del almacén principal, y la limpieza se olvida en algún camino de código que nadie prueba. Centralizar esa lógica en una clase es para lo que sirve el patrón.
Cuándo Usar
Usa el Patrón Manager cuando:
- Un grupo de objetos relacionados necesita gestión centralizada de ciclo de vida — creación, búsqueda, mutación y eliminación pasan por un solo lugar.
- Las restricciones o invariantes tienen que cumplirse sobre toda la colección — IDs únicos, límites por grupo, ese tipo de regla.
- Los objetos se buscan por ID o clave en lugar de mantenerse por referencia directa, y quieres una instancia canónica por clave (un identity map).
- Los recursos necesitan pooling, caché o reuso — conexiones, threads, sprites, sesiones.
- Varios llamadores comparten la misma colección y importa la consistencia entre el almacén y sus índices.
Cuándo Evitar
- Un objeto único que nunca se coordina con sus pares no necesita un manager.
- Cuando una colección o un arreglo simple basta, la estructura extra solo estorba.
- Si el manager empieza a manejar responsabilidades no relacionadas, se ha convertido en un God object — divídelo.
- Cuando un contenedor de DI o un registro de servicios ya te da la misma coordinación, no construyas un mecanismo paralelo. Consulta el Patrón Dependency Injection para ese enfoque. Para la variante de capa web que actúa de fachada ante los managers, mira el Patrón Business Delegate.
Solución
Python
from typing import Dict, List, Optional
from dataclasses import dataclass, field
import uuid
@dataclass
class Employee:
name: str
department: str
salary: float
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
active: bool = True
class EmployeeManager:
"""Centraliza el ciclo de vida y operaciones de objetos Employee"""
def __init__(self):
self._employees: Dict[str, Employee] = {}
self._department_index: Dict[str, List[str]] = {}
def hire(self, name: str, department: str, salary: float) -> Employee:
emp = Employee(name=name, department=department, salary=salary)
self._employees[emp.id] = emp
self._department_index.setdefault(department, []).append(emp.id)
return emp
def get(self, emp_id: str) -> Optional[Employee]:
return self._employees.get(emp_id)
def fire(self, emp_id: str) -> bool:
emp = self._employees.get(emp_id)
if emp:
emp.active = False
self._department_index[emp.department].remove(emp_id)
del self._employees[emp_id]
return True
return False
def list_by_department(self, department: str) -> List[Employee]:
ids = self._department_index.get(department, [])
return [e for e in (self._employees.get(eid) for eid in ids)
if e is not None and e.active]
def total_payroll(self) -> float:
return sum(emp.salary for emp in self._employees.values() if emp.active)
def give_raise(self, emp_id: str, percentage: float) -> bool:
emp = self._employees.get(emp_id)
if emp:
emp.salary *= (1 + percentage / 100)
return True
return False
# Uso
manager = EmployeeManager()
emp1 = manager.hire("Alice", "Engineering", 120000)
emp2 = manager.hire("Bob", "Engineering", 95000)
emp3 = manager.hire("Carol", "Sales", 80000)
print(f"Nómina total: ${manager.total_payroll():,.2f}")
print(f"Equipo Engineering: {[e.name for e in manager.list_by_department('Engineering')]}")
manager.give_raise(emp1.id, 10)
print(f"Nuevo salario de Alice: ${manager.get(emp1.id).salary:,.2f}")
Java
import java.util.*;
class Employee {
private final String id;
private String name;
private String department;
private double salary;
private boolean active = true;
public Employee(String name, String department, double salary) {
this.id = UUID.randomUUID().toString().substring(0, 8);
this.name = name;
this.department = department;
this.salary = salary;
}
public String getId() { return id; }
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
public void setSalary(double salary) { this.salary = salary; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
}
class EmployeeManager {
private final Map<String, Employee> employees = new HashMap<>();
private final Map<String, List<String>> deptIndex = new HashMap<>();
public Employee hire(String name, String department, double salary) {
Employee emp = new Employee(name, department, salary);
employees.put(emp.getId(), emp);
deptIndex.computeIfAbsent(department, k -> new ArrayList<>()).add(emp.getId());
return emp;
}
public Employee get(String id) { return employees.get(id); }
public boolean fire(String id) {
Employee emp = employees.get(id);
if (emp != null) {
emp.setActive(false);
deptIndex.getOrDefault(emp.getDepartment(), new ArrayList<>()).remove(id);
employees.remove(id);
return true;
}
return false;
}
public List<Employee> listByDepartment(String department) {
List<String> ids = deptIndex.getOrDefault(department, List.of());
List<Employee> result = new ArrayList<>();
for (String id : ids) {
Employee emp = employees.get(id);
if (emp != null && emp.isActive()) result.add(emp);
}
return result;
}
public double totalPayroll() {
return employees.values().stream()
.filter(Employee::isActive)
.mapToDouble(Employee::getSalary)
.sum();
}
}
// Uso
EmployeeManager manager = new EmployeeManager();
Employee alice = manager.hire("Alice", "Engineering", 120000);
manager.hire("Bob", "Engineering", 95000);
System.out.println("Nómina total: $" + manager.totalPayroll());
JavaScript
class Employee {
constructor(name, department, salary) {
this.id = Math.random().toString(36).substring(2, 10);
this.name = name;
this.department = department;
this.salary = salary;
this.active = true;
}
}
class EmployeeManager {
constructor() {
this.employees = new Map();
this.deptIndex = new Map();
}
hire(name, department, salary) {
const emp = new Employee(name, department, salary);
this.employees.set(emp.id, emp);
if (!this.deptIndex.has(department)) {
this.deptIndex.set(department, []);
}
this.deptIndex.get(department).push(emp.id);
return emp;
}
get(id) {
return this.employees.get(id);
}
fire(id) {
const emp = this.employees.get(id);
if (emp) {
emp.active = false;
const dept = this.deptIndex.get(emp.department) || [];
const idx = dept.indexOf(id);
if (idx > -1) dept.splice(idx, 1);
this.employees.delete(id);
return true;
}
return false;
}
listByDepartment(department) {
const ids = this.deptIndex.get(department) || [];
return ids.map(id => this.employees.get(id)).filter(e => e && e.active);
}
totalPayroll() {
let total = 0;
for (const emp of this.employees.values()) {
if (emp.active) total += emp.salary;
}
return total;
}
giveRaise(id, percentage) {
const emp = this.employees.get(id);
if (emp) {
emp.salary *= (1 + percentage / 100);
return true;
}
return false;
}
}
// Uso
const manager = new EmployeeManager();
const alice = manager.hire('Alice', 'Engineering', 120000);
manager.hire('Bob', 'Engineering', 95000);
manager.hire('Carol', 'Sales', 80000);
console.log('Nómina total:', manager.totalPayroll());
console.log('Engineering:', manager.listByDepartment('Engineering').map(e => e.name));
Explicación
El manager asume cuatro trabajos que de otro modo se filtrarían al código cliente:
- Creación:
hire()construye el objeto y lo registra en un solo paso, así que un objeto nunca puede existir sin ser rastreado. - Búsqueda:
get()ylist_by_department()responden consultas sin exponer el almacenamiento interno. - Mutación:
give_raise()modifica objetos gestionados a través del manager, lo que mantiene los invariantes exigibles en un único punto de control. - Limpieza:
fire()elimina el objeto y sus entradas en el índice — el orden importa, porque quitar primero del almacén dejaría el índice apuntando a la nada.
Hay dos consecuencias aquí que suelen dar problemas en producción, así que merecen una mirada más de cerca.
El manager es un identity map. Dos llamadas a get("a1b2c3d4") devuelven la misma instancia, así que una mutación hecha a través de una referencia es visible por todas las demás. Normalmente es lo que quieres — un Employee canónico por ID — pero también abre la puerta a bugs de aliasing: código que muta un objeto que pidió puede sorprender a otro llamador que tiene la misma instancia.
La consistencia de índices es la parte difícil. El ejemplo mantiene un segundo mapa de departamento a IDs de empleado. Cada camino de escritura — hire, fire, un hipotético transfer — tiene que actualizar ambas estructuras, y el manager es el único lugar donde esa disciplina se puede imponer. Si el manager corre en un contexto multi-thread, aquí es también donde va el locking: ConcurrentHashMap en Java, un threading.Lock alrededor de los métodos de mutación en Python, o una convención de escritor único. Centralizar el control de concurrencia es posiblemente uno de los mayores beneficios del patrón, porque que cada llamador haga su propio locking nunca funciona.
Variantes
| Variante | Responsabilidad | Ejemplo |
|---|---|---|
| Entity Manager | CRUD + consultas | JPA EntityManager, Django ORM Manager |
| Resource Manager | Pooling y reuso | Pools de conexiones, thread pools |
| Scene Manager | Ciclo de vida en juegos | Unity SceneManager, managers de objetos de juego |
| Service Manager | Registro y búsqueda | Android ServiceManager, registros de plugins |
| Cache Manager | Expulsión y TTL | Wrappers de caché Redis, cachés en memoria |
Lo que funciona
- Mantén los managers enfocados. Un manager, un tipo de objeto. Un
UserManagerque además manda emails y renderiza dashboards es un God class en formación. - Devuelve vistas inmutables. Entrega copias o wrappers de solo lectura de las colecciones internas; un llamador que puede mutar
deptIndexpuede corromperlo en silencio. - Actualiza cada índice en cada escritura. Un índice obsoleto es peor que no tener índice: devuelve respuestas incorrectas con total confianza.
- Diseña el caso de fallo a propósito. Decide si
get()devuelveOptional,nullo lanza excepción, y documéntalo. Los llamadores construirán suposiciones alrededor de lo que elijas. - Soporta operaciones por lotes cuando el dominio lo pida.
hire_many()yfire_all_in_department()son comunes en cuanto la colección crece.
Errores Comunes
- El manager se convierte en God class. Si gestiona objetos no relacionados, divídelo antes de que el acoplamiento se endurezca.
- Exponer estado interno. Devolver una referencia modificable al mapa interno permite a los llamadores saltarse cada invariante que montaste.
- Olvidar limpiar los índices. Quitar del almacén pero no del índice deja IDs fantasma que reaparecen en los resultados de las consultas.
- I/O síncrono dentro de métodos del manager. Si
get()toca la red o el disco, delega ese trabajo — los managers en contextos async bloquean el event loop entero. - Sobre-gestionar objetos triviales. Para un puñado de elementos usados en un solo sitio, una lista simple es más clara que una clase manager.
Ejemplos del Mundo Real
EntityManager (JPA)
El EntityManager de Jakarta Persistence es el manager canónico: persiste, busca, fusiona y elimina entidades mientras mantiene una caché de primer nivel y rastrea los cambios pendientes por transacción. La especificación de persistencia documenta cómo el ciclo de vida de las entidades — new, managed, detached, removed — es exactamente la coordinación de ciclo de vida que describe este patrón.
Android ServiceManager
El ServiceManager de Android registra los servicios del sistema (window manager, package manager, sensores) por nombre y entrega referencias binder a los llamadores. Un registro central, un contrato de búsqueda, y ninguna instanciación directa por parte de los clientes.
Managers de escenas y assets en motores de juegos
Motores como Unity centralizan el ciclo de vida de las escenas detrás de SceneManager, y la mayoría de motores aplican la misma idea a sprites, cuerpos físicos y fuentes de audio — cada manager es dueño de la creación, las actualizaciones por frame y la destrucción de su tipo de objeto.
Managers de Django
Los Managers de Django se sitúan entre los modelos y la base de datos: Question.objects.filter(...) pasa por el manager, que mantiene la construcción de consultas fuera de la clase del modelo.
Preguntas frecuentes
¿Cuál es la diferencia entre un Manager y un Repository?
Un Repository abstrae la persistencia — hace que una base de datos parezca una colección. Un Manager es más amplio: coordina el ciclo de vida, impone invariantes y puede contener reglas de negocio sobre la colección. En la práctica se solapan, y muchos repositorios se implementan como managers sobre un data mapper.
¿En qué se diferencia un Manager de un contenedor de inyección de dependencias?
Un contenedor de DI gestiona servicios — normalmente singletons resueltos por tipo en el arranque. Un Manager suele gestionar objetos de dominio creados y destruidos en tiempo de ejecución. Si tu única necesidad es "dame la única instancia de X", la inyección de dependencias es la herramienta más simple.
¿Puede un manager ser un singleton?
A menudo lo es — una única fuente de verdad para un tipo de recurso encaja bien con el Patrón Singleton. Pero prefiere inyectar el manager en lugar de tirar de una global; mantiene los tests honestos y te permite acotar managers por request o por tenant más adelante.
¿Cómo hago unit tests de código que usa un manager?
Pon el manager detrás de una interfaz o un protocolo pequeño (hire, get, fire) y dale a la unidad bajo prueba una implementación fake en memoria. Probar el propio manager es sencillo: no tiene dependencias externas en este diseño, así que las aserciones de hire/fire/índices son unit tests puros.
¿Quién es dueño de la seguridad frente a concurrencia — el manager o sus llamadores?
El manager, siempre. Los llamadores no pueden coordinarse entre sí, así que el locking por llamador solo protege de las carreras de ese llamador. Pon el lock dentro de los métodos de mutación, o usa colecciones concurrentes, y documenta la garantía.
¿Qué debería hacer el manager cuando no encuentra un objeto?
Elige un contrato y manténlo. Devolver Optional/null funciona cuando los objetos ausentes son rutinarios; lanzar un NotFoundError conviene cuando un ID que falta señala un bug. Mezclar ambas entre métodos es peor que cualquiera de las dos opciones por separado.
Recursos Relacionados
Patrón Facade
Provee una interfaz simplificada a un subsistema complejo. Un patrón estructural que oculta detalles de implementación detrás de una API limpia.
PatternPatrón Singleton: Ejemplos en Java, Python y TypeScript
Garantiza que una clase tenga una única instancia y ofrece acceso global a ella — el patrón creacional para controlar la creación de objetos, con implementaciones listas para producción.
PatternPatrón Dependency Injection
Suministra dependencias desde fuera en lugar de crearlas internamente. Un patrón arquitectural para código desacoplado y testeable.
PatternPatrón Business Delegate
Reduce el acoplamiento entre capas de presentación y negocio introduciendo un intermediario que maneja lookup, creación e invocación de servicios de negocio.
PatternPatrón Context Object: Ejemplos
Reduce la proliferación de parámetros pasando un único objeto de contexto por la cadena de llamadas. Ejemplos en Python, Java y JavaScript.
PatternPatrón Repository
Abstrae la lógica de acceso a datos detrás de una interfaz limpia. Patrón de diseño arquitectural para capas de datos testeables y mantenibles.