El Prototype es un patrón que crea objetos nuevos clonando una instancia existente. Evita que el cliente dependa de la clase concreta y puede ahorrar trabajo cuando preparar el objeto inicial resulta costoso.
Imagina que estás desarrollando un videojuego de estrategia y tienes que generar un ejército de 1000 orcos. Si cada construcción repite cálculos costosos, clonar una configuración ya preparada puede ser más barato que reconstruirla. Los recursos pesados, como las texturas, normalmente se comparten mediante una caché.
Podemos crear un orco base, configurarlo una vez y pedir al sistema que lo copie. El coste real dependerá de qué datos se clonen y cuáles se compartan.
El Concepto
El patrón especifica que los objetos que queramos copiar deben implementar una interfaz (normalmente llamada IPrototype o ICloneable) que declare un método Clone().
De esta forma, el código cliente no necesita saber qué clase concreta está copiando. Solo sabe que si llama a Clone(), obtiene una réplica exacta.
El Gran Dilema: Shallow Copy vs Deep Copy
Aquí es donde el patrón se complica y donde la mayoría de los bugs aparecen. Cuando copiamos un objeto, ¿qué estamos copiando realmente?
Supongamos que nuestro objeto tiene dentro una referencia a otro objeto (ej: un Usuario que tiene una Direccion).
Copia Superficial (Shallow Copy)
Copiamos los valores de los campos del objeto.
- Si el campo es un valor (int, float), se copia el valor.
- Si el campo es una referencia (una clase), se copia la referencia (el puntero), no el objeto al que apunta.
Resultado: El objeto original y el clon comparten la misma Direccion. Si modificas la calle en el clon, ¡se cambia también en el original! Esto suele ser un desastre.
Copia Profunda (Deep Copy)
Copiamos el objeto y, recursivamente, copiamos también todos los objetos a los que referencia.
- Se crea una nueva instancia de
Direccionpara el clon.
Resultado: Los objetos son totalmente independientes. Esto es lo que solemos querer, pero es más costoso de procesar.
Implementación en C#
En .NET tenemos la interfaz ICloneable, pero tiene mala fama porque no especifica si la copia debe ser superficial o profunda. Nosotros vamos a hacerlo explícito.
Ejemplo: El problema de la Copia Superficial
Usaremos el método MemberwiseClone() de C#, que realiza una copia superficial automática.
public class InfoID
{
public int IdNumber;
public InfoID(int id) => IdNumber = id;
}
public class Persona
{
public int Edad;
public string Nombre;
public InfoID IdInfo; // Referencia a otro objeto
public Persona ShallowCopy()
{
// MemberwiseClone crea una copia superficial:
// Copia 'Edad' y 'Nombre' bien.
// Pero copia la REFERENCIA de 'IdInfo'.
return (Persona)this.MemberwiseClone();
}
}Si usamos esto:
Persona p1 = new Persona();
p1.IdInfo = new InfoID(666);
Persona p2 = p1.ShallowCopy();
p2.IdInfo.IdNumber = 999; // Modificamos el clon
Console.WriteLine(p1.IdInfo.IdNumber); // ¡Imprime 999! Hemos roto el original.Ejemplo: Implementando Deep Copy
Para hacerlo bien, debemos asegurarnos de clonar también las dependencias.
public class Persona
{
public int Edad;
public string Nombre;
public InfoID IdInfo;
// Método para copia profunda
public Persona DeepCopy()
{
// 1. Hacemos la copia superficial primero
Persona clon = (Persona)this.MemberwiseClone();
// 2. CREAMOS manualmente las nuevas instancias de las referencias
clon.IdInfo = new InfoID(this.IdInfo.IdNumber);
// Si InfoID tuviera más objetos dentro, tendríamos que clonarlos también.
// clon.Nombre es un string, y los strings en C# son inmutables,
// así que no hace falta clonarlos explícitamente.
return clon;
}
}Una técnica muy usada en C# para hacer Deep Copy rápido (aunque no la más eficiente) es serializar el objeto a JSON y deserializarlo inmediatamente. Al reconstruirlo, se crean todas las instancias nuevas.
Registro de Prototipos (Prototype Registry)
A veces no clonamos un objeto que acabamos de crear, sino que tenemos un “catálogo” de objetos predefinidos.
Imagina un editor de niveles. Tenéis una paleta con tipos de enemigos: “Orco Débil”, “Orco Fuerte”, “Jefe Orco”. No creáis uno nuevo cada vez, le pedís al registro: “Dame una copia del Orco Fuerte”.
public class RegistroMonstruos
{
private Dictionary<string, Monstruo> _prototipos = new Dictionary<string, Monstruo>();
public RegistroMonstruos()
{
// Inicializamos los prototipos base
_prototipos.Add("Fantasma", new Monstruo { Tipo = "Fantasma", Ataque = 10 });
_prototipos.Add("Demonio", new Monstruo { Tipo = "Demonio", Ataque = 50 });
}
public Monstruo CrearMonstruo(string tipo)
{
// Devolvemos un CLON, no el original
return _prototipos[tipo].DeepCopy();
}
}Cuándo usar Prototype
- Optimización: Cuando el coste de inicializar un objeto es alto (BBDD, Red, cálculos complejos).
- Configuración: Cuando tienes muchas instancias similares y solo cambian un par de parámetros. Es más fácil clonar y modificar lo distinto que construir desde cero.
- Independencia: Cuando quieres desacoplar tu sistema de la jerarquía de clases de los objetos que crea (similar al Factory, pero clonando).