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 Java, la clonación puede implementarse copiando manualmente propiedades, usando interfaces como Cloneable
o apoyándose en 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 Java muestra un prototipo de tarjeta de identificación. Implementa Cloneable
y realiza una copia superficial.
public class Credencial implements Cloneable {
private String nombre;
private String rol;
private List<String> permisos;
public Credencial(String nombre, String rol, List<String> permisos) {
this.nombre = nombre;
this.rol = rol;
this.permisos = permisos;
}
public String nombre() {
return nombre;
}
public String rol() {
return rol;
}
public List<String> permisos() {
return permisos;
}
public void establecerNombre(String nombre) {
this.nombre = nombre;
}
@Override
public Credencial clone() {
try {
return (Credencial) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError("Clonación no soportada", e);
}
}
}
Al clonar, la lista de permisos es compartida. Para escenarios donde los permisos deben ser independientes, se necesita clonación profunda.
Una alternativa segura consiste en implementar un constructor copia que genere nuevas instancias para los campos mutables.
public class CredencialCompleta implements Cloneable {
private String nombre;
private String rol;
private List<String> permisos;
public CredencialCompleta(String nombre, String rol, List<String> permisos) {
this.nombre = nombre;
this.rol = rol;
this.permisos = new ArrayList<>(permisos);
}
private CredencialCompleta(CredencialCompleta original) {
this(original.nombre, original.rol, original.permisos);
}
public void establecerRol(String rol) {
this.rol = rol;
}
public List<String> permisos() {
return Collections.unmodifiableList(permisos);
}
@Override
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.
public class Vehiculo implements Cloneable {
private String marca;
private String modelo;
private String color;
private Motor motor;
public Vehiculo(String marca, String modelo, String color, Motor motor) {
this.marca = marca;
this.modelo = modelo;
this.color = color;
this.motor = motor;
}
private Vehiculo(Vehiculo original) {
this(original.marca, original.modelo, original.color, original.motor.clone());
}
public void establecerColor(String color) {
this.color = color;
}
public void establecerModelo(String modelo) {
this.modelo = modelo;
}
@Override
public Vehiculo clone() {
return new Vehiculo(this);
}
@Override
public String toString() {
return marca + " " + modelo + " (" + color + ", " + motor + ")";
}
}
public class Motor implements Cloneable {
private final int cilindrada;
private final String tipo;
public Motor(int cilindrada, String tipo) {
this.cilindrada = cilindrada;
this.tipo = tipo;
}
@Override
public Motor clone() {
try {
return (Motor) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
@Override
public String toString() {
return cilindrada + "cc " + tipo;
}
}
public class RegistroVehiculos {
private final Map<String, Vehiculo> prototipos = new HashMap<>();
public void registrar(String clave, Vehiculo vehiculo) {
prototipos.put(clave, vehiculo);
}
public Vehiculo obtener(String clave) {
Vehiculo prototipo = prototipos.get(clave);
if (prototipo == null) {
throw new IllegalArgumentException("No existe prototipo para la clave: " + clave);
}
return prototipo.clone();
}
}
El cliente configura el registro con prototipos y obtiene copias personalizadas sin conocer las clases concretas:
public class Aplicacion {
public static void main(String[] args) {
RegistroVehiculos registro = new RegistroVehiculos();
registro.registrar("sedan", new Vehiculo("AutoYa", "Sedán LX", "gris", new Motor(1600, "nafta")));
registro.registrar("suv", new Vehiculo("AutoYa", "SUV Familiar", "negro", new Motor(2200, "diesel")));
Vehiculo vehiculoCliente = registro.obtener("sedan");
vehiculoCliente.establecerColor("azul");
vehiculoCliente.establecerModelo("Sedán LX Plus");
System.out.println(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 Cloneable
en Java requiere un cuidado especial debido a sus peculiaridades (como la necesidad de llamar a super.clone()
).
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.