El patrón Prototype propone crear nuevos objetos copiando instancias existentes llamadas prototipos. En lugar de instanciar clases directamente con new
, se clona un objeto configurado previamente, lo que resulta útil cuando la inicialización es costosa o cuando el tipo concreto debe seleccionarse en tiempo de ejecución.
Aplicar Prototype permite construir catálogos de objetos listos para usar y personalizarlos sin acoplar el código cliente a clases concretas. Es especialmente valioso en sistemas que crean gran cantidad de objetos similares o que requieren preservar configuraciones complejas.
Instanciar clases con estructuras profundas, dependencias externas u operaciones pesadas puede ser costoso. Además, cuando se reciben objetos desde configuraciones o se desea permitir extensiones por parte del usuario, el código cliente no siempre conoce el tipo exacto a crear.
Prototype permite registrar objetos base y clonarlos cuando se necesiten nuevas instancias. Esto evita repetir inicializaciones costosas y reduce el acoplamiento con las clases concretas. Cada clon puede ajustarse según sea necesario sin afectar el prototipo original.
La intención del patrón es especificar los tipos de objetos a crear mediante una instancia prototípica que se clona. El cliente solicita un prototipo ya configurado y obtiene una copia funcional, sobre la cual puede aplicar cambios adicionales.
Los prototipos pueden registrarse en un repositorio o registry, donde cada entrada representa una configuración del objeto. Esto habilita un catálogo que puede extenderse en tiempo de ejecución sin tocar el código compilado.
Los roles principales en Prototype son:
new
.En C#, la clonación puede implementarse recurriendo al método protegido MemberwiseClone
, definiendo interfaces como ICloneable
o empleando constructores copia. La decisión depende de cuán profunda deba ser la copia.
La clonación superficial copia los valores directos de las propiedades, pero mantiene referencias compartidas a objetos mutables. Es adecuada cuando los colaboradores son inmutables o se comparten de forma intencional.
La clonación profunda crea copias independientes de todos los objetos compuestos que forman parte del prototipo. Asegura que el clon no comparta estado con el original, a costa de mayor complejidad.
Seleccionar la estrategia correcta es crucial para evitar efectos secundarios. Un prototipo que sea modificado por error puede afectar a otros elementos si la clonación es superficial cuando debería ser profunda.
El siguiente ejemplo en C# muestra un prototipo de credencial. Implementa ICloneable
y realiza una copia superficial, por lo que las listas se comparten entre instancias.
using System;
using System.Collections.Generic;
public class Credencial : ICloneable
{
public Credencial(string nombre, string rol, List<string> permisos)
{
Nombre = nombre;
Rol = rol;
Permisos = permisos;
}
public string Nombre { get; private set; }
public string Rol { get; }
public List<string> Permisos { get; }
public void EstablecerNombre(string nombre)
{
Nombre = nombre;
}
public object Clone()
{
return MemberwiseClone();
}
}
Al clonar, la lista de permisos se comparte. Si cada credencial debe mantener sus propios permisos, es necesario pasar a una clonación profunda.
Una alternativa segura consiste en implementar un constructor copia que genere nuevas instancias para los campos mutables.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
public class CredencialCompleta
{
public CredencialCompleta(string nombre, string rol, IEnumerable<string> permisos)
{
Nombre = nombre;
Rol = rol;
Permisos = new List<string>(permisos);
}
private CredencialCompleta(CredencialCompleta original)
{
Nombre = original.Nombre;
Rol = original.Rol;
Permisos = new List<string>(original.Permisos);
}
public string Nombre { get; private set; }
public string Rol { get; private set; }
public List<string> Permisos { get; }
public IReadOnlyList<string> PermisosLectura => new ReadOnlyCollection<string>(Permisos);
public void EstablecerRol(string rol)
{
Rol = rol;
}
public CredencialCompleta Clone()
{
return new CredencialCompleta(this);
}
}
El constructor copia asegura que cada clon tenga su propia lista de permisos, evitando que cambios en un clon afecten a otros.
Implementaremos un registro de prototipos que permite obtener distintas configuraciones de vehículos (sedán, SUV, deportivo). Cada prototipo se clona y se personaliza con datos específicos.
using System;
using System.Collections.Generic;
public class Vehiculo
{
public Vehiculo(string marca, string modelo, string color, Motor motor)
{
Marca = marca;
Modelo = modelo;
Color = color;
Motor = motor;
}
private Vehiculo(Vehiculo original)
{
Marca = original.Marca;
Modelo = original.Modelo;
Color = original.Color;
Motor = original.Motor.Clone();
}
public string Marca { get; private set; }
public string Modelo { get; private set; }
public string Color { get; private set; }
public Motor Motor { get; private set; }
public void EstablecerColor(string color)
{
Color = color;
}
public void EstablecerModelo(string modelo)
{
Modelo = modelo;
}
public Vehiculo Clone()
{
return new Vehiculo(this);
}
public override string ToString()
{
return $"{Marca} {Modelo} ({Color}, {Motor})";
}
}
public class Motor
{
public Motor(int cilindrada, string tipo)
{
Cilindrada = cilindrada;
Tipo = tipo;
}
public int Cilindrada { get; }
public string Tipo { get; }
public Motor Clone()
{
return new Motor(Cilindrada, Tipo);
}
public override string ToString()
{
return $"{Cilindrada}cc {Tipo}";
}
}
public class RegistroVehiculos
{
private readonly Dictionary<string, Vehiculo> _prototipos = new Dictionary<string, Vehiculo>();
public void Registrar(string clave, Vehiculo vehiculo)
{
_prototipos[clave] = vehiculo;
}
public Vehiculo Obtener(string clave)
{
if (!_prototipos.TryGetValue(clave, out var prototipo))
{
throw new ArgumentException($"No existe prototipo para la clave: {clave}", nameof(clave));
}
return prototipo.Clone();
}
}
El cliente configura el registro con prototipos y obtiene copias personalizadas sin conocer las clases concretas:
using System;
public static class DemoPrototype
{
public static void Main()
{
var registro = new RegistroVehiculos();
registro.Registrar("sedan", new Vehiculo("AutoYa", "Sedan LX", "gris", new Motor(1600, "nafta")));
registro.Registrar("suv", new Vehiculo("AutoYa", "SUV Familiar", "negro", new Motor(2200, "diesel")));
var vehiculoCliente = registro.Obtener("sedan");
vehiculoCliente.EstablecerColor("azul");
vehiculoCliente.EstablecerModelo("Sedan LX Plus");
Console.WriteLine(vehiculoCliente);
}
}
El prototipo original permanece intacto en el registro, mientras que el cliente obtiene una copia independiente que puede modificar según sus preferencias.
La clonación puede ser compleja si el objeto tiene dependencias no clonables o recursos compartidos. Es necesario decidir qué partes se copian y cuáles se referencian, lo que aumenta el esfuerzo de mantenimiento. Además, el uso de ICloneable
en C# requiere cuidado porque devuelve object
y suele complementarse con métodos tipados o constructores copia para evitar conversiones inseguras.
En escenarios donde la inicialización es trivial o el número de combinaciones es reducido, Prototype puede resultar innecesario. Evaluar siempre la relación costo-beneficio antes de implementar la clonación.
Considere este patrón cuando:
Prototype complementa a Abstract Factory y Builder: una fábrica puede devolver prototipos precargados y un builder puede iniciarse a partir de un prototipo para aplicar ajustes finales.