Los cuatro patrones anteriores crean objetos a partir de una clase: eligiéndola (fábricas), restringiéndola (Singleton) o montándola paso a paso (Builder). Prototype cambia el punto de partida: el objeto nuevo no nace de una clase sino de otro objeto, copiándolo. Parece un truco menor hasta que te encuentras el caso de uso perfecto —en PideYa lo tenemos: el botón "repetir mi último pedido" y las plantillas de carta de los restaurantes— y hasta que descubres que clonar bien en Java es un campo de minas con nombre propio: Cloneable. Esta lección cubre el patrón, sus dos profundidades de copia, los problemas del mecanismo estándar de Java y las alternativas que usarás en la práctica.

Contenido

  1. El problema en PideYa: repetir pedido y plantillas de carta
  2. Intención y estructura del patrón
  3. Clonación superficial vs. profunda
  4. El mecanismo estándar de Java: Cloneable y sus problemas
  5. La alternativa recomendada: constructores de copia
  6. Otras alternativas de copia en Java
  7. Registro de prototipos
  8. Cuándo usarlo y cuándo no
  9. Errores comunes, ejercicios y conclusión

El problema en PideYa: repetir pedido y plantillas de carta

Caso 1 — "Repetir mi último pedido". Es el botón más rentable de la app: un toque y el cliente vuelve a pedir lo del viernes pasado. La primera implementación reconstruía el pedido campo a campo:

// Versión ingenua: copiar a mano, campo a campo. NO imitar.
Pedido repetido = Pedido.builder(anterior.getCliente(), anterior.getRestaurante())
        .entregaEnDomicilio(anterior.getDireccionEntrega())
        .franja(anterior.getFranja())
        .instrucciones(anterior.getInstrucciones())
        // ...¿y las líneas? ¿y los cubiertos? ¿y el teléfono de contacto?
        .build();

El dolor: cada vez que Pedido gana un atributo, alguien tiene que acordarse de añadirlo aquí. En el sprint 14 se añadió telefonoContacto y nadie actualizó esta copia: los pedidos repetidos perdían el teléfono en silencio. Es el síntoma que anotamos en la introducción del módulo: crear un objeto "casi igual" a otro cuesta reconstruirlo entero, y la copia manual se desincroniza de la clase.

Caso 2 — Plantillas de carta. PideYa ofrece a los restaurantes nuevos cartas de partida por tipo de cocina ("pizzería", "japonés", "hamburguesería"), ya montadas con secciones y productos típicos, que el restaurante duplica y personaliza. La carta plantilla es un objeto grande (secciones → productos → opciones), costoso de montar desde base de datos; lo natural es tener el ejemplar montado y duplicarlo para cada restaurante que lo pida.

Ambos casos comparten la esencia: el mejor "plano" para el objeto nuevo es un objeto que ya existe.

Intención y estructura del patrón

Intención (GoF): especificar los tipos de objetos a crear usando una instancia prototípica, y crear los nuevos objetos copiando ese prototipo.

La estructura es la más simple del módulo: una interfaz con una sola operación, "clónate", que cada clase implementa sabiendo copiarse a sí misma.

classDiagram
    class Prototipo~T~ {
        <<interface>>
        +clonar() T
    }
    class Pedido {
        +clonar() Pedido
    }
    class CartaRestaurante {
        +clonar() CartaRestaurante
    }
    class ServicioRepetirPedido {
        +repetir(Pedido) Pedido
    }
    Prototipo <|.. Pedido
    Prototipo <|.. CartaRestaurante
    ServicioRepetirPedido ..> Prototipo : pide clones\nsin conocer la clase
Rol GoF Papel En PideYa
Prototype Interfaz que declara la operación de clonado Prototipo<T> con clonar()
ConcretePrototype Sabe copiarse a sí mismo (decidiendo qué y cómo) Pedido, CartaRestaurante
Client Crea objetos pidiendo clones, sin new ni clases concretas ServicioRepetirPedido, alta de restaurantes

Definimos la interfaz con genéricos para que cada clase devuelva su propio tipo, sin casts:

public interface Prototipo<T extends Prototipo<T>> {
    /** Devuelve una copia independiente de este objeto. */
    T clonar();
}

Dos ventajas estructurales del patrón antes de bajar al barro: el cliente crea objetos sin conocer su clase concreta (un List<Prototipo<?>> de plantillas se clona uniformemente, venga cada una de donde venga), y el estado del ejemplar viaja gratis (la carta "pizzería" clonada ya trae sus 40 productos; con una fábrica habría que reconstruirlos). El diablo, como siempre, está en el verbo "copiar".

Clonación superficial vs. profunda

Es LA distinción de esta lección. Una copia superficial (shallow) duplica el objeto pero comparte los objetos referenciados; una copia profunda (deep) duplica también (recursivamente) lo referenciado que sea mutable.

classDiagram
    direction LR
    class CartaOriginal {
        nombre = "Pizzería base"
    }
    class CartaClonSuperficial {
        nombre = "Pizzería Da Luigi"
    }
    class ListaSecciones {
        LA MISMA lista
        para ambas cartas
    }
    CartaOriginal --> ListaSecciones
    CartaClonSuperficial --> ListaSecciones : ¡peligro compartido!

El accidente que esto produce, con nuestra plantilla de carta:

CartaRestaurante plantilla = plantillas.get("pizzeria");
CartaRestaurante cartaDaLuigi = plantilla.clonarSuperficial();   // copia superficial

cartaDaLuigi.getSecciones().get(0).añadirProducto(pizzaDeLaCasa);
// Sorpresa: la PLANTILLA "pizzería" ahora también ofrece la pizza de Da Luigi,
// y con ella todos los restaurantes que se den de alta a partir de hoy.

La regla para decidir la profundidad, campo a campo:

Tipo de campo ¿Copia necesaria?
Primitivos (int, boolean...) La copia superficial ya los duplica: nada que hacer
Referencias a inmutables (String, BigDecimal, LocalDate, records inmutables, enums) Compartirlas es seguro e incluso deseable: nada que hacer
Referencias a mutables (colecciones, objetos con setters) Copia profunda obligatoria si el clon debe ser independiente
Referencias a entidades compartidas a propósito (el Restaurante, el Cliente) Compartir es lo correcto: clonar aquí sería un error de dominio

Fíjate en la última fila: profundidad no significa "clonarlo todo". Al repetir un pedido, el clon debe apuntar al mismo cliente y restaurante (son entidades del mundo, no partes del pedido), pero necesita su propia lista de líneas. Decidir dónde parar la copia es diseño, no mecánica; la distinción composición/agregación de la lección de UML es exactamente el mapa: lo compuesto (rombo negro) se clona, lo agregado (rombo blanco) se comparte.

El mecanismo estándar de Java: Cloneable y sus problemas

Java trae clonación de serie: el método protegido Object.clone() y la interfaz marcadora Cloneable. Su aspecto:

public class CartaRestaurante implements Cloneable {

    private String nombre;
    private List<Seccion> secciones = new ArrayList<>();

    @Override
    public CartaRestaurante clone() {
        try {
            CartaRestaurante copia = (CartaRestaurante) super.clone(); // copia superficial
            // Arreglo manual de lo mutable para hacerla profunda:
            copia.secciones = new ArrayList<>();
            for (Seccion s : this.secciones) {
                copia.secciones.add(s.clone());     // exige que Seccion repita todo esto
            }
            return copia;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);            // imposible: somos Cloneable
        }
    }
}

Funciona, pero el mecanismo está universalmente señalado como uno de los diseños fallidos del lenguaje (Bloch le dedica un capítulo demoledor en Effective Java). Sus problemas, catalogados:

  • Cloneable no declara clone(): es una interfaz vacía que solo cambia el comportamiento de un método protected heredado de Object. No puedes escribir Cloneable c = ...; c.clone(); — la interfaz no te da el método. Un contrato invisible.
  • super.clone() es superficial siempre: hace copia binaria campo a campo. Todo lo mutable queda compartido salvo que lo arregles a mano, y olvidar un campo es un bug silencioso (el accidente de la carta).
  • CloneNotSupportedException es checked: te obliga al try/catch ritual aunque sea imposible que salte.
  • Se salta los constructores: el clon nace sin pasar por ningún constructor, esquivando invariantes, validaciones y campos final que quisieras recalcular. Malísimo casamiento con las clases inmutables del estilo Builder.
  • Frágil con la herencia: si una subclase añade campos mutables y olvida redefinir clone(), hereda una copia superficial rota de los suyos.

Conclusión práctica honesta: conoce Cloneable para leerlo (aparece en código antiguo y en el JDK: los arrays y ArrayList lo usan), pero no lo elijas para código nuevo. El patrón Prototype no exige Cloneable: exige una operación de copia, y hay formas mejores de dársela.

La alternativa recomendada: constructores de copia

Un constructor de copia es un constructor que recibe otra instancia y copia de ella. Todo explícito, sin excepciones rituales, pasando por el constructor (invariantes intactas), y compatible con campos final:

public final class Pedido implements Prototipo<Pedido> {

    private final Cliente cliente;                 // compartido: entidad
    private final Restaurante restaurante;         // compartido: entidad
    private final List<LineaPedido> lineas;        // PROPIO: copiar en profundidad
    private final Direccion direccionEntrega;      // inmutable: compartible
    private final String instrucciones;            // inmutable: compartible
    // ... resto de campos de la lección anterior ...

    /** Constructor de copia: la política de profundidad, explícita y en un solo lugar. */
    private Pedido(Pedido original) {
        this.cliente = original.cliente;                    // compartir (agregación)
        this.restaurante = original.restaurante;            // compartir (agregación)
        this.lineas = original.lineas.stream()              // profundizar (composición)
                .map(LineaPedido::new)                      //  <- copia de cada línea
                .collect(Collectors.toCollection(ArrayList::new));
        this.direccionEntrega = original.direccionEntrega;  // inmutable: compartir
        this.instrucciones = original.instrucciones;
        // ...
    }

    @Override
    public Pedido clonar() {
        return new Pedido(this);
    }
}

public class LineaPedido {
    private final Producto producto;   // entidad de la carta: compartir
    private int cantidad;              // primitivo mutable: viaja con la copia

    /** Constructor de copia de la línea. */
    public LineaPedido(LineaPedido original) {
        this.producto = original.producto;
        this.cantidad = original.cantidad;
    }
    // ...
}

Y el caso de uso queda en dos líneas, inmune a futuros campos nuevos (el constructor de copia vive en la clase, así que quien añada un campo lo tiene delante y el olvido es mucho más difícil que en una copia externa):

public class ServicioRepetirPedido {

    public Pedido repetir(Pedido anterior) {
        Pedido nuevo = anterior.clonar();     // todo el estado viaja: líneas, dirección...
        return nuevo.conFranja(null)          // ajustes del nuevo contexto: la franja
                    .conFechaCreacion(LocalDateTime.now());  // y la fecha no se heredan
    }
}

(Los métodos conX(...) —withers— devuelven una copia con ese campo cambiado: el complemento natural de Prototype sobre objetos inmutables. Los records de Java los hacen triviales.)

Otras alternativas de copia en Java

  • Métodos de fabricación de copia: public static Pedido copiaDe(Pedido p) — idéntico al constructor de copia con nombre más expresivo.
  • Clonar vía builder: si la clase ya tiene Builder, un método toBuilder() que devuelve un builder precargado con el estado actual une lo mejor de ambos patrones: anterior.toBuilder().franja(otraFranja).build(). Es la forma más idiomática de "copiar con retoques" en Java moderno (Lombok la genera con @Builder(toBuilder = true)).
  • Records con with-semántica: para objetos de valor pequeños, un record inmutable + withers cubre el 90% de las necesidades de copia sin patrón explícito.
  • Serializar y deserializar (a JSON o binario): copia profunda "automática" de todo el grafo. Útil en utilidades genéricas de test, pero lenta, frágil ante campos no serializables y sin control de la política compartir/copiar: no la uses como mecanismo de dominio.

Registro de prototipos

Cuando los prototipos son un catálogo gestionado —exactamente nuestras plantillas de carta—, el patrón se completa con un registro: un contenedor que asocia claves a ejemplares prototípicos y entrega clones (nunca el original) a quien pida:

public class RegistroPlantillasCarta {

    private final Map<String, CartaRestaurante> plantillas = new ConcurrentHashMap<>();

    /** Se puebla en el arranque... o en caliente, sin desplegar código nuevo. */
    public void registrar(String clave, CartaRestaurante plantilla) {
        plantillas.put(clave, plantilla);
    }

    /** SIEMPRE entrega un clon: el ejemplar maestro queda intocable. */
    public CartaRestaurante crear(String clave) {
        CartaRestaurante plantilla = plantillas.get(clave);
        if (plantilla == null) {
            throw new IllegalArgumentException("No hay plantilla: " + clave);
        }
        return plantilla.clonar();
    }
}

// Arranque de PideYa:
registro.registrar("pizzeria", cartaPizzeriaBase);
registro.registrar("japones", cartaJaponesBase);

// Alta de un restaurante: un objeto grande, montado y listo, sin conocer clases
CartaRestaurante carta = registro.crear("pizzeria");
carta.setNombre("Pizzería Da Luigi");

Compara con el registro de fábricas de la lección de Factory Method: la estructura es gemela (un mapa de clave → forma de crear), pero allí se registraban recetas (Supplier) y aquí se registran ejemplares. La diferencia práctica es que un ejemplar puede configurarse con datos, en tiempo de ejecución (un gestor de contenidos podría componer una plantilla nueva desde un panel de administración y registrarla al vuelo), mientras que una receta nueva exige código nuevo. Esa es la ventaja diferencial de Prototype: nuevas "clases de facto" sin nuevas clases.

Cuándo usarlo y cuándo no

Úsalo cuando:

  • El objeto nuevo se parece mucho a uno existente y copiarlo es más simple (y más robusto) que reconstruirlo: "repetir pedido".
  • Los ejemplares son caros de montar (base de datos, cálculo) y clonar amortiza el coste: plantillas de carta.
  • Las variantes de producto se definen por configuración de estado más que por comportamiento distinto: docenas de plantillas no justifican docenas de clases; un prototipo por plantilla, sí.
  • El cliente debe crear objetos sin conocer sus clases concretas y no quieres una jerarquía paralela de fábricas.

No lo uses cuando:

  • Los objetos son pequeños y baratos de construir: new o un builder son más claros que razonar sobre profundidades de copia.
  • El grafo del objeto tiene referencias circulares o recursos no copiables (conexiones, ficheros abiertos): clonar se vuelve un pantano.
  • Lo que varía entre "ejemplares" es comportamiento, no estado: eso pide subclases o Strategy, no clones.

Relación con otros patrones (solo mención): el registro de prototipos es un Factory Method/simple factory cuyo mecanismo interno es el clonado, y una Abstract Factory puede implementarse clonando un juego de prototipos por familia; Memento usa copias de estado con otro fin (deshacer); Composite y Decorator producen estructuras que a menudo conviene clonar completas.

Errores Comunes y Consejos

  • Copia superficial accidental: el error número uno. Cada campo mutable compartido sin querer es una bomba de relojería que explota lejos del clon (el accidente de la plantilla de carta). Revisa la tabla de la sección 3 campo a campo.
  • Clonar de más: duplicar el Restaurante al repetir un pedido crearía dos "verdades" sobre la misma entidad. La pregunta correcta nunca es "¿cómo lo clono todo?" sino "¿dónde termina este objeto?" (composición vs. agregación).
  • Copiar el estado que no debe viajar: identificadores únicos, fechas de creación, estado del flujo (un pedido YA_ENTREGADO clonado no puede nacer entregado). Define en el constructor de copia qué se resetea, no solo qué se copia.
  • Implementar Cloneable "porque es lo estándar": ya viste el catálogo de trampas. En código nuevo: constructor de copia o toBuilder().
  • Registro que entrega el original: si crear() devuelve la plantilla sin clonar, el primer restaurante que personalice su carta redecora la plantilla de todos. El registro clona siempre.
  • Consejo: escribe un test de independencia por cada clase clonable: clona, muta el clon a fondo y verifica que el original no cambió (y viceversa). Es barato y caza las copias superficiales accidentales antes que producción.

Ejercicios

Ejercicio 1: política de copia de CartaRestaurante

CartaRestaurante tiene: String nombre, Restaurante propietario, List<Seccion> secciones (y cada Seccion: String titulo, List<Producto> productos, donde Producto tiene precio editable por restaurante). Decide, campo a campo y con justificación, qué se comparte y qué se copia en profundidad al clonar una plantilla para un restaurante nuevo, y escribe el constructor de copia de CartaRestaurante y Seccion.

Ejercicio 2: cazar el bug de copia

Este código de "repetir pedido" salió a producción. ¿Qué bug tiene y cómo se manifiesta?

public Pedido repetir(Pedido anterior) {
    Pedido nuevo = new Pedido(anterior.getCliente(), anterior.getRestaurante(),
                              anterior.getLineas(),          // <-- atención aquí
                              anterior.getDireccionEntrega());
    return nuevo;
}
// Después, en el flujo de la app:
nuevo.getLineas().removeIf(l -> !l.getProducto().estaDisponible());

Ejercicio 3: repetir pedido, completo

Usando el Pedido con constructor de copia de la lección, implementa ServicioRepetirPedido.repetir(Pedido anterior) cumpliendo: (a) las líneas cuyo producto ya no esté disponible en la carta se eliminan del clon; (b) la franja horaria no se hereda; (c) si ninguna línea sobrevive, lanzar PedidoNoRepetibleException.

Soluciones

Solución 1: nombre — String inmutable, se comparte la referencia (y normalmente se sobreescribe justo después). propietario — entidad agregada; en el clon ni siquiera se copia el de la plantilla: se asigna el nuevo restaurante (o null hasta la asignación). secciones — composición pura: copia profunda. productos dentro de cada sección — como el precio es editable por restaurante, cada carta necesita sus propios productos: profundidad también aquí (si los productos fueran inmutables y el precio viviera fuera, compartirlos sería lo correcto: la política depende del diseño del dominio, no hay respuesta mecánica).

private CartaRestaurante(CartaRestaurante original, Restaurante nuevoPropietario) {
    this.nombre = original.nombre;
    this.propietario = nuevoPropietario;
    this.secciones = original.secciones.stream()
            .map(Seccion::new)
            .collect(Collectors.toCollection(ArrayList::new));
}

public Seccion(Seccion original) {
    this.titulo = original.titulo;
    this.productos = original.productos.stream()
            .map(Producto::new)                 // Producto necesita SU constructor de copia
            .collect(Collectors.toCollection(ArrayList::new));
}

Solución 2: anterior.getLineas() pasa la misma lista al pedido nuevo (copia superficial de facto). El removeIf posterior elimina los productos no disponibles... también del pedido original: el historial del cliente muta retroactivamente (su pedido del viernes "pierde" líneas), y si el original estaba en pantalla o en caché, muestra datos corruptos. Manifestación típica: incidencias intermitentes de "mi pedido antiguo ha cambiado solo". Corrección: copia profunda de las líneas en el constructor (o clonar() de la lección).

Solución 3:

public class ServicioRepetirPedido {

    public Pedido repetir(Pedido anterior) {
        Pedido clon = anterior.clonar();                       // copia profunda de líneas

        clon.getLineasModificables()
            .removeIf(l -> !l.getProducto().estaDisponible()); // (a) solo afecta al clon

        if (clon.getLineas().isEmpty()) {
            throw new PedidoNoRepetibleException(              // (c)
                    "Ningún producto del pedido sigue disponible");
        }
        return clon.conFranja(null);                           // (b) la franja no se hereda
    }
}

La gracia está en (a): podemos podar líneas del clon con total tranquilidad porque la copia es profunda; con la versión del ejercicio 2, este mismo código corrompería el historial.

Conclusión

Prototype completa el repertorio creacional con su punto de partida único: copiar un ejemplar en vez de instanciar una clase, con dos casos de PideYa donde es la solución natural (repetir pedido, plantillas de carta) y un registro que convierte los ejemplares en un catálogo vivo, ampliable con datos y no solo con código. Lo esencial que te llevas es criterio, más que mecánica: la frontera superficial/profunda se decide campo a campo (compartir entidades, copiar composiciones, resetear lo que no debe viajar), Cloneable se lee pero no se escribe, y el constructor de copia —o toBuilder()— es el vehículo sano en Java.

Con esto, los cinco patrones creacionales están sobre la mesa: control de instancias, fábricas de productos y de familias, construcción paso a paso y clonación. Falta lo más importante: saber elegir entre ellos, ver cómo se combinan y repasar qué quedó instalado en cada rincón de PideYa. Es el trabajo de la última lección del módulo: Comparativa y Elección de Patrones Creacionales.

Curso de Patrones de Diseño de Software

Módulo 1: Introducción a los Patrones de Diseño

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados