Patrón Prototype: Clona Objetos sin Reconstruirlos
Crea objetos clonando instancias preconfiguradas en lugar de reconstruirlos desde cero. Patrón Prototype con ejemplos en Python, Java y JavaScript.
Visión General
Un juego genera doscientos enemigos que solo difieren en vida, velocidad y arma. Reconstruir cada uno con un constructor significa releer la configuración, resolver los assets otra vez y repetir la misma inicialización doscientas veces. El patrón Prototype evita eso: conserva una instancia ya construida y estampa objetos nuevos copiándola.
Esta es la entrada de catálogo del patrón tal como lo definieron los Gang of Four — qué es, cuándo compensa y dónde falla. Para un recorrido aplicado con un registro de configuraciones, mira la receta de clonación.
Cuándo Usarlo
Usa el patrón Prototype cuando:
- Construir un objeto es caro: parsea archivos, toca la red o cablea un grafo de objetos grande.
- Necesitas muchos objetos que difieren solo ligeramente, como variantes de enemigos o plantillas de documento.
- La clase concreta a instanciar no se conoce hasta el runtime — tienes una instancia configurada, no una clase.
- Quieres capturar el estado actual de un objeto y seguir mutando el original.
El caso del tipo en runtime es el que la gente pasa por alto: un sistema de plugins que recibe “un objeto para copiar” no puede llamar a new UnknownClass(), pero sí puede llamar a clone() sobre la instancia que le entregaron. Prototype desacopla “qué crear” de “qué clase sabe cómo”, algo que los constructores no pueden hacer.
Sáltalo cuando los objetos son baratos de construir o inmutables. Si tus objetos son tipos valor inmutables, clonar no te compra nada; puedes compartir la instancia directamente, que es para lo que están los records de Java y los dataclasses de Python con frozen=True.
Solución
Toda implementación expone un método clone() que devuelve una copia del receptor. La sutileza está en qué significa “copia” — las copias superficiales comparten objetos anidados, las profundas los duplican.
Python
import copy
from abc import ABC, abstractmethod
class Document(ABC):
def __init__(self, content="", formatting=None):
self.content = content
self.formatting = formatting or {}
@abstractmethod
def clone(self):
pass
class Report(Document):
def __init__(self, content="", formatting=None, sections=None):
super().__init__(content, formatting)
self.sections = sections or []
def clone(self):
# Copia profunda para que las estructuras anidadas queden independientes
return copy.deepcopy(self)
# Construir el prototipo una vez
prototype = Report(
content="Plantilla de Informe Anual",
formatting={"font": "Arial", "size": 12},
sections=["Introducción", "Finanzas", "Conclusión"],
)
# Estampar variantes
report_a = prototype.clone()
report_a.content = "Informe Anual 2024"
report_a.sections.append("Apéndice")
report_b = prototype.clone()
report_b.formatting["size"] = 14
print(report_a.formatting is report_b.formatting) # False — copia profunda
copy.deepcopy maneja estructuras anidadas y referencias circulares, pero no es gratis: recorre el grafo de objetos entero. En rutas calientes, implementa __copy__/__deepcopy__ a mano y copia solo los campos que de verdad lo necesitan.
JavaScript
class Report {
constructor(content = "", formatting = {}, sections = []) {
this.content = content;
this.formatting = formatting;
this.sections = sections;
}
clone() {
// structuredClone hace una copia profunda (navegadores, Node 17+)
return structuredClone(this);
}
}
const prototype = new Report(
"Plantilla de Informe Anual",
{ font: "Arial", size: 12 },
["Introducción", "Finanzas", "Conclusión"]
);
const reportA = prototype.clone();
reportA.sections.push("Apéndice");
const reportB = prototype.clone();
reportB.formatting.size = 14;
console.log(reportA.formatting === reportB.formatting); // false
Ojo con los límites de structuredClone: copia Map, Set, Date, ArrayBuffer y referencias circulares, pero lanza excepción con funciones y nodos DOM, y las instancias de clase pierden su cadena de prototipos: reportA instanceof Report devuelve false. Si el clon debe conservar métodos, escribe clone() a mano: return new Report(this.content, {...this.formatting}, [...this.sections]).
Java
public class Report implements Cloneable {
private String content;
private Map<String, Object> formatting;
private List<String> sections;
public Report(String content, Map<String, Object> formatting,
List<String> sections) {
this.content = content;
this.formatting = new HashMap<>(formatting);
this.sections = new ArrayList<>(sections);
}
@Override
public Report clone() {
// Copiar los campos mutables; String es inmutable y se puede compartir
return new Report(content, new HashMap<>(formatting), new ArrayList<>(sections));
}
}
El Cloneable/Object.clone() incorporado en Java hace una copia superficial — las listas y mapas clonados siguen compartidos. Escribe tu propio clone() o constructor de copia que duplique los campos mutables. Prefiere constructores de copia o factorías estáticas; Cloneable está considerada una interfaz rota porque en realidad no declara clone().
Explicación
El patrón tiene dos roles:
- Prototype — declara
clone()(una interfaz, una clase base o simplemente una convención). - Prototipos concretos — implementan
clone()para devolver una copia usable de sí mismos.
Un registro de prototipos es el compañero habitual: un mapa de prototipos con nombre que los llamadores clonan bajo demanda, de modo que el sistema ni siquiera necesita conocer las clases concretas, solo las claves del registro.
Lo difícil no es la firma del método; es la semántica de la copia. Una copia superficial duplica el objeto pero conserva las referencias a los mismos objetos anidados; muta reportA.formatting y reportB cambia también. Una copia profunda duplica el grafo entero, más seguro pero más lento, y falla con lo que no se puede copiar: descriptores de archivo abiertos, sockets, hilos, closures. Decide por campo: comparte lo inmutable o caro, copia lo mutable.
Las referencias circulares merecen mención porque rompen las implementaciones ingenuas: el objeto A referencia a B que referencia a A, y un clon recursivo entra en bucle infinito o desborda la pila. copy.deepcopy y structuredClone manejan los ciclos correctamente; un copiador recursivo escrito a mano normalmente no.
Aquí está el bug de la copia superficial en su forma más pequeña — dos clones “independientes” que comparten una lista:
class Enemy:
def __init__(self, name, abilities):
self.name = name
self.abilities = abilities
def clone(self):
# Superficial: abilities se comparte con todos los clones
return Enemy(self.name, self.abilities)
grunt = Enemy("grunt", ["melee"])
elite = grunt.clone()
elite.name = "elite"
elite.abilities.append("fireball")
print(grunt.abilities) # ['melee', 'fireball'] — el prototipo también cambió
Un list(self.abilities) en clone() lo arregla. Esa única línea es la diferencia entre “el patrón funciona” y “doscientos enemigos comparten la misma lista de habilidades”.
Coste y límites
Clonar cambia coste de construcción por coste de copia, y el cambio no siempre sale a favor. copy.deepcopy recorre el grafo de objetos entero por reflexión, así que en un árbol grande suele ser más lento que el constructor que pretendía evitar. structuredClone es más rápido pero sigue siendo lineal en el tamaño del grafo. El patrón compensa cuando la inicialización hace trabajo que la copia se salta: parseo, I/O, resolución de dependencias. Si construir es solo asignar campos, una llamada al constructor gana al recorrido del grafo. Mide ambos caminos antes de adoptar Prototype “por rendimiento”; el resultado habitual es que el setup caro se amortiza y el barato no.
El registro de prototipos
Cuando un sistema conserva varios prototipos con nombre, el compañero estándar es un registro: un mapa de nombre a instancia configurada que devuelve un clon nuevo en cada consulta.
class ConfigRegistry {
private prototypes = new Map<string, ProductConfig>();
register(key: string, config: ProductConfig): void {
this.prototypes.set(key, config);
}
get(key: string): ProductConfig | undefined {
// Cada llamador recibe un clon independiente
return this.prototypes.get(key)?.clone();
}
}
const registry = new ConfigRegistry();
registry.register("pro", new ProductConfig("Pro", 29.99, "software"));
const custom = registry.get("pro");
custom?.setAttribute("discount", "20%"); // el prototipo registrado queda intacto
Los llamadores nunca ven una clase concreta ni un constructor — solo una clave y un clon. Ese desacoplamiento es la ganancia real cuando los tipos de objeto se deciden por configuración en runtime.
Dónde Aparece
El patrón se esconde dentro de frameworks que probablemente ya usas. El @Scope("prototype") de Spring entrega a cada punto de inyección una instancia de bean nueva en lugar del singleton compartido: semántica de prototipo sin llamar a clone(). El Object.Instantiate(prefab) de Unity clona un prefab configurado: es literalmente el flujo de trabajo entero del motor para generar enemigos, balas y partículas. El módulo copy de Python existe precisamente porque el patrón es lo bastante común para necesitar soporte en la stdlib, y el Cloneable de Java es la lección de qué pasa al añadirlo a posteriori: una interfaz que no declara nada y un Object.clone() que lanza excepción salvo que la interfaz marcadora esté presente.
Variantes
| Variante | Caso de uso | Compromiso |
|---|---|---|
| Clon superficial | Objetos planos, o estado compartido intencional | Rápido; las mutaciones se filtran entre clones |
| Clon profundo | Estado mutable anidado | Más lento; falla con recursos no copiables |
| Registro de prototipos | Prototipos preconfigurados con nombre | Catálogo central; indirección extra |
| Clon por serialización | Copiar vía serializar/deserializar | Maneja grafos; lento, pierde estado no serializable |
| Constructor de copia | new Report(other) | Explícito y type-safe; uno por clase |
Lo que Funciona
- Elige superficial vs profunda por campo. Copia las colecciones mutables, comparte los escalares inmutables y los recursos de solo lectura caros.
- Implementa
clone()explícitamente en lugar de confiar en las opciones por defecto del lenguaje — elObject.clone()de Java y el{...obj}de JavaScript son ambos superficiales. - Mantén los prototipos inmutables tras registrarlos. Un registro cuyas entradas se mutan produce clones de un blanco en movimiento.
- Deja que los objetos inmutables se salten la clonación. Si el objeto no puede cambiar, clonar es trabajo perdido — devuelve
this.
Errores Comunes
- Clonar algo que un llamador mutará de vuelta. Si el prototipo en sí es editable tras el registro, cada clon nuevo hereda lo que la última mutación dejó. Trata los prototipos registrados como de solo lectura.
- Copia superficial de estado mutable anidado. El bug clásico de Prototype:
reportA.sections.push("X")aparece enreportB. Si los clones deben ser independientes, los mutables anidados necesitan copias profundas. - Asumir que
structuredCloneconserva clases. Devuelve objetos planos —instanceoffalla y los métodos desaparecen. - Clonar recursos vivos. Descriptores de archivo, conexiones de base de datos y sockets no se pueden clonar de forma significativa; reábrelos o recréalos dentro de
clone(). - Recursión sobre referencias circulares. Los copiadores profundos manuales necesitan un conjunto de visitados; las versiones de la stdlib ya lo tienen.
- Usar Prototype cuando encaja una factoría. Si la creación es barata y parametrizada, una factoría o un builder es más simple que mantener un prototipo mutable.
Lecturas Adicionales
- MDN: structuredClone() — qué copia el clon profundo y qué lanza excepción.
- Refactoring.Guru: Prototype — la entrada de catálogo canónica con diagramas.
- Documentación de Python: módulo copy — semántica de
copyvsdeepcopy. - Ejemplos companion — registro de prototipos ejecutable en Python y JavaScript con tests.
Preguntas frecuentes
¿Cuál es la diferencia entre Prototype y Factory?
Factory crea objetos a través de un método de creación dedicado; Prototype los crea copiando una instancia existente. Factory responde "constrúyeme una X con estos parámetros"; Prototype responde "dame otra igual que esta". Si la construcción es barata, Factory suele ser más simple.
¿Cuándo es clonar realmente más rápido que construir?
Cuando la inicialización hace I/O o preparación pesada, como parsear un archivo de configuración, cargar una plantilla, resolver un grafo de dependencias. Clonar un objeto en memoria se salta todo eso. Para objetos planos con unos pocos campos, el constructor gana siempre; mide antes de adoptar el patrón por rendimiento.
¿Debo usar siempre copia profunda?
No. La copia profunda es la opción segura por defecto para estado mutable anidado, pero es más lenta y falla con recursos no copiables. Si los objetos anidados son inmutables o el estado compartido es intencional, la copia superficial es correcta; solo documenta cuál realiza clone() para que los llamadores no se lleven sorpresas.
¿Cómo se relaciona Prototype con Memento?
Ambos capturan estado de objeto, pero por razones distintas: Prototype clona para producir objetos nuevos de trabajo, mientras Memento captura estado para restaurarlo después. Si estás versionando o deshaciendo, ese es el trabajo de Memento.
¿Cuenta la cadena de prototipos de JavaScript como este patrón?
En realidad no: misma palabra, idea distinta. El patrón GoF trata de copia a nivel de instancia vía clone(); la cadena de prototipos de JS es delegación para búsqueda de propiedades. structuredClone y la copia con spread son los mecanismos de JS que sí implementan el patrón.
¿Cómo clono un objeto que contiene una conexión de BD o un archivo abierto?
No lo clonas; lo restableces. Los recursos no copiables hay que readquirirlos dentro de clone() (abrir una conexión nueva, reabrir el archivo) o excluirlos de la copia por completo. Un objeto clonado apuntando al socket del original es un bug que solo sale a la luz bajo carga.
Recursos Relacionados
Patron Factory
Crea objetos sin especificar la clase exacta a instanciar. Un patrón de diseño creacional para la creación flexible de objetos.
PatternPatrón Builder
Construye objetos complejos paso a paso. Patrón de diseño creacional para construcción de objetos legible y configurable.
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 Memento
Captura y restaura el estado interno de un objeto sin violar el encapsulamiento. Un patrón de comportamiento para deshacer/rehacer.
PatternPrototype Pattern para Clonacion de Objetos y Plantillas
Crea nuevos objetos copiando existentes, permitiendo plantillas pre-configuradas y evitando explosion de subclases cuando la creacion de objetos es costosa