Programación · Patrones de diseño
Patrón Prototype explicado con ejemplos
Respuesta corta
Prototype es un patrón de diseño creacional que crea objetos nuevos copiando una instancia existente, el prototipo, en lugar de construirlos desde cero con new. Cada prototipo implementa un método clone(), de modo que el código cliente puede duplicar un objeto sin conocer su clase concreta. Es útil cuando los objetos son costosos de configurar o vienen en muchas variantes preestablecidas. La trampa principal es la copia superficial: los objetos anidados también deben copiarse, o el clon y el original los compartirán.
Intención
Especificar los tipos de objetos que se van a crear mediante una instancia prototípica, y crear objetos nuevos copiando ese prototipo. El patrón Prototype es uno de los cinco patrones creacionales del libro de la Banda de los Cuatro (GoF).
El problema que resuelve
Imagina un editor de diagramas con una paleta de formas predefinidas: un cuadro de advertencia rojo, una nota azul, una insignia verde de «hecho». Cada preajuste tiene un tamaño, colores, una fuente y una lista de reglas de estilo. Cuando el usuario arrastra un preajuste al lienzo, el editor necesita una forma nueva e independiente que empiece exactamente con esa configuración.
Sin el patrón, el editor necesitaría una función de creación distinta para cada preajuste y tendría que conocer todas las clases concretas de formas. Peor aún, los preajustes definidos por el usuario en tiempo de ejecución no podrían crearse en absoluto, porque ningún código conoce de antemano su configuración. Con Prototype, el editor simplemente llama a preset.clone().
Estructura y participantes
| Participante | Responsabilidad | En el ejemplo |
|---|---|---|
| Prototype | Declara la interfaz para clonarse a sí mismo. | Shape con clone() |
| ConcretePrototype | Implementa clone() copiando su propio estado. | Box |
| Client | Crea objetos nuevos pidiendo a un prototipo que se clone. | el código del editor |
| Registro de prototipos (opcional) | Guarda prototipos con nombre para que los clientes puedan buscarlos. | mapa palette |
Implementación en TypeScript
clone() copia todos los campos, incluidos los objetos y arreglos anidados, de modo que la copia es independiente del original:
interface Shape {
clone(): Shape;
describe(): string;
}
class Box implements Shape {
constructor(
public width: number,
public height: number,
public style: { fill: string; border: string },
public tags: string[],
) {}
clone(): Box {
// copy nested objects too, so the clone shares nothing mutable
return new Box(this.width, this.height, { ...this.style }, [...this.tags]);
}
describe(): string {
return `${this.width}x${this.height} ${this.style.fill} [${this.tags.join(", ")}]`;
}
}
// A registry of named prototypes, which could also be filled at run time.
const palette = new Map<string, Shape>([
["warning", new Box(120, 40, { fill: "red", border: "black" }, ["alert"])],
["note", new Box(160, 80, { fill: "blue", border: "none" }, [])],
]);
const a = palette.get("warning")!.clone() as Box;
a.width = 200;
a.tags.push("urgent");
console.log(a.describe()); // 200x40 red [alert, urgent]
console.log(palette.get("warning")!.describe()); // 120x40 red [alert]El editor nunca nombra Box. Un preajuste nuevo es una entrada nueva en el registro, no una clase nueva ni una función de creación nueva.
Copia superficial frente a copia profunda
El error más común con Prototype es un clon que comparte objetos anidados con el original. Una copia con el operador spread es superficial: solo copia el nivel superior.
const original = { name: "note", style: { fill: "blue" } };
const shallow = { ...original };
shallow.style.fill = "green";
console.log(original.style.fill); // green (shared object was changed)
const deep = structuredClone(original);
deep.style.fill = "yellow";
console.log(original.style.fill); // green (unchanged)structuredClone() viene incorporada en los navegadores modernos y en Node.js y hace copias profundas de datos simples, incluidos objetos anidados, arreglos, Map, Set y Date. Tiene dos límites que importan aquí:
- Las instancias de clase vuelven como objetos simples: la copia pierde su prototipo, por lo que faltan métodos como
describe(). - Las funciones y los nodos del DOM no se pueden clonar;
structuredClone()lanza unDataCloneError.
Cuándo usarlo y cuándo no
Usa el patrón Prototype cuando:
- Los objetos sean costosos de preparar (cargados desde un archivo, calculados, configurados en muchos pasos) y necesites muchas copias parecidas.
- El conjunto de variantes se defina en tiempo de ejecución, por ejemplo preajustes o plantillas creados por los usuarios.
- Quieras crear objetos sin depender de sus clases concretas y sin una jerarquía paralela de fábricas.
Evítalo cuando:
- Los objetos sean baratos y simples de crear. Llamar a un constructor o a una función de fábrica es más claro.
- Los objetos contengan recursos que no deben duplicarse, como archivos abiertos, sockets o identificadores únicos. Clonarlos requiere un tratamiento especial.
- Los objetos tengan referencias circulares y una propiedad compleja, lo que dificulta hacer bien la copia profunda.
Usos en el mundo real
- JavaScript es en sí mismo un lenguaje basado en prototipos:
Object.create(proto)crea un objeto nuevo que hereda de uno existente. Esto está relacionado con el patrón pero no es idéntico, porque el objeto nuevo delega en su prototipo en lugar de copiarlo. - El DOM:
node.cloneNode(true)hace una copia profunda de un elemento y sus hijos, una forma habitual de generar fragmentos de interfaz repetidos. - Java: las clases pueden implementar la interfaz
Cloneabley sobrescribirObject.clone(), aunque muchos desarrolladores de Java prefieren los constructores de copia. - Los motores de juegos y los editores suelen generar objetos nuevos a partir de plantillas preconfiguradas (a menudo llamadas prefabs).
Patrones relacionados
- Patrón Abstract Factory: una fábrica puede guardar un conjunto de prototipos y crear productos clonándolos, en lugar de necesitar una subclase de fábrica por familia.
- Patrón Factory Method: crea objetos mediante subclases; Prototype evita esas subclases copiando instancias.
- Patrones Composite y Decorator: los diseños que los usan mucho suelen beneficiarse de Prototype, porque copiar una estructura configurada es más fácil que reconstruirla.
- Patrón Memento: también copia el estado de un objeto, pero para restaurarlo después, no para crear un objeto nuevo e independiente.
Preguntas frecuentes
¿El patrón Prototype es lo mismo que los prototipos de JavaScript?
No. Los prototipos de JavaScript son un mecanismo del lenguaje para la herencia: un objeto delega en su prototipo las propiedades que le faltan. El patrón Prototype crea una copia independiente de un objeto.
¿clone() debe hacer una copia profunda o superficial?
Normalmente profunda para los datos anidados mutables, de modo que el clon y el original no se afecten entre sí. Los valores inmutables y los recursos compartidos de solo lectura pueden seguir compartidos sin problema.
¿Puedo usar structuredClone() para implementar clone()?
Solo para objetos de datos simples. Para instancias de clase, structuredClone() devuelve un objeto simple sin métodos, así que escribe tú mismo la lógica de copia o reconstruye la instancia con su constructor.
¿Cuándo es mejor Prototype que una fábrica?
Cuando las variantes son muchas, se configuran en tiempo de ejecución o son costosas de preparar. Clonar una instancia ya hecha es entonces más simple que escribir una función de creación para cada variante.