10. Prototype (Prototipo) - Patrón Creacional

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.

10.1 Problema que resuelve

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.

10.2 Intención y motivación

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.

10.3 Estructura y participantes

Los roles principales en Prototype son:

  • Prototype: interfaz o clase abstracta que declara el método de clonación.
  • Concrete Prototype: clases que implementan la clonación y devuelven copias de sí mismas.
  • Client: solicita instancias copiando prototipos en lugar de usar new.
  • Prototype Registry (opcional): almacena y administra la colección de prototipos disponibles.

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.

10.4 Clonación superficial vs. profunda

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.

10.5 Implementación en Java con clonación superficial

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.

10.6 Clonación profunda con constructor copia

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.

10.7 Ejemplo completo en Java: registro de prototipos

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.

Prototype

10.8 Ventajas y beneficios

  • Reduce el impacto de crear objetos costosos al reutilizar instancias configuradas.
  • Permite agregar tipos en tiempo de ejecución simplemente registrando nuevos prototipos.
  • Evita acoplar el cliente a clases concretas; basta con trabajar con la interfaz de clonación.
  • Facilita la configuración en herramientas visuales o archivos, ya que los prototipos pueden ser definidos externamente.

10.9 Desventajas y cautelas

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.

10.10 Cuándo elegir Prototype

Considere este patrón cuando:

  • El costo de crear objetos desde cero es alto y conviene reutilizar configuraciones base.
  • Se necesita crear objetos en tiempo de ejecución sin conocer su tipo concreto de antemano.
  • Se requiere un catálogo de configuraciones que pueda ampliarse sin recompilar.
  • Los objetos deben clonarse frecuentemente y mantener independencia entre sus copias.

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.