Els quatre patrons anteriors creen objectes a partir d'una classe: triant-la (fàbriques), restringint-la (Singleton) o muntant-la pas a pas (Builder). Prototype canvia el punt de partida: l'objecte nou no neix d'una classe sinó d'un altre objecte, copiant-lo. Sembla un truc menor fins que et trobes el cas d'ús perfecte —a PideYa el tenim: el botó "repetir la meva darrera comanda" i les plantilles de carta dels restaurants— i fins que descobreixes que clonar bé en Java és un camp de mines amb nom propi: Cloneable. Aquesta lliçó cobreix el patró, les seves dues profunditats de còpia, els problemes del mecanisme estàndard de Java i les alternatives que usaràs a la pràctica.
Contingut
- El problema a PideYa: repetir comanda i plantilles de carta
- Intenció i estructura del patró
- Clonació superficial vs. profunda
- El mecanisme estàndard de Java:
Cloneablei els seus problemes - L'alternativa recomanada: constructors de còpia
- Altres alternatives de còpia en Java
- Registre de prototips
- Quan usar-lo i quan no
- Errors comuns, exercicis i conclusió
El problema a PideYa: repetir comanda i plantilles de carta
Cas 1 — "Repetir la meva darrera comanda". És el botó més rendible de l'app: un toc i el client torna a demanar allò del divendres passat. La primera implementació reconstruïa la comanda camp a camp:
// Versio ingenua: copiar a ma, camp a camp. NO imitar.
Comanda repetida = Comanda.builder(anterior.getClient(), anterior.getRestaurant())
.lliuramentADomicili(anterior.getAdrecaLliurament())
.franja(anterior.getFranja())
.instruccions(anterior.getInstruccions())
// ...i les linies? i els coberts? i el telefon de contacte?
.build();El dolor: cada cop que Comanda guanya un atribut, algú s'ha de recordar d'afegir-lo aquí. Al sprint 14 es va afegir telefonContacte i ningú no va actualitzar aquesta còpia: les comandes repetides perdien el telèfon en silenci. És el símptoma que vam anotar a la introducció del mòdul: crear un objecte "gairebé igual" a un altre costa reconstruir-lo sencer, i la còpia manual es dessincronitza de la classe.
Cas 2 — Plantilles de carta. PideYa ofereix als restaurants nous cartes de partida per tipus de cuina ("pizzeria", "japonès", "hamburgueseria"), ja muntades amb seccions i productes típics, que el restaurant duplica i personalitza. La carta plantilla és un objecte gran (seccions → productes → opcions), costós de muntar des de base de dades; el natural és tenir l'exemplar muntat i duplicar-lo per a cada restaurant que el demani.
Tots dos casos comparteixen l'essència: el millor "plànol" per a l'objecte nou és un objecte que ja existeix.
Intenció i estructura del patró
Intenció (GoF): especificar els tipus d'objectes a crear usant una instància prototípica, i crear els nous objectes copiant aquest prototip.
L'estructura és la més simple del mòdul: una interfície amb una sola operació, "clona't", que cada classe implementa sabent copiar-se a si mateixa.
classDiagram
class Prototip~T~ {
<<interface>>
+clonar() T
}
class Comanda {
+clonar() Comanda
}
class CartaRestaurant {
+clonar() CartaRestaurant
}
class ServeiRepetirComanda {
+repetir(Comanda) Comanda
}
Prototip <|.. Comanda
Prototip <|.. CartaRestaurant
ServeiRepetirComanda ..> Prototip : demana clons\nsense coneixer la classe
| Rol GoF | Paper | A PideYa |
|---|---|---|
| Prototype | Interfície que declara l'operació de clonatge | Prototip<T> amb clonar() |
| ConcretePrototype | Sap copiar-se a si mateix (decidint què i com) | Comanda, CartaRestaurant |
| Client | Crea objectes demanant clons, sense new ni classes concretes |
ServeiRepetirComanda, alta de restaurants |
Definim la interfície amb genèrics perquè cada classe retorni el seu propi tipus, sense casts:
public interface Prototip<T extends Prototip<T>> {
/** Retorna una copia independent d'aquest objecte. */
T clonar();
}Dos avantatges estructurals del patró abans de baixar al fang: el client crea objectes sense conèixer la seva classe concreta (un List<Prototip<?>> de plantilles es clona uniformement, vingui cadascuna d'on vingui), i l'estat de l'exemplar viatja gratis (la carta "pizzeria" clonada ja porta els seus 40 productes; amb una fàbrica caldria reconstruir-los). El diable, com sempre, és en el verb "copiar".
Clonació superficial vs. profunda
És LA distinció d'aquesta lliçó. Una còpia superficial (shallow) duplica l'objecte però comparteix els objectes referenciats; una còpia profunda (deep) duplica també (recursivament) allò referenciat que sigui mutable.
classDiagram
direction LR
class CartaOriginal {
nom = "Pizzeria base"
}
class CartaClonSuperficial {
nom = "Pizzeria Da Luigi"
}
class LlistaSeccions {
LA MATEIXA llista
per a totes dues cartes
}
CartaOriginal --> LlistaSeccions
CartaClonSuperficial --> LlistaSeccions : perill compartit!
L'accident que això produeix, amb la nostra plantilla de carta:
CartaRestaurant plantilla = plantilles.get("pizzeria");
CartaRestaurant cartaDaLuigi = plantilla.clonarSuperficial(); // copia superficial
cartaDaLuigi.getSeccions().get(0).afegirProducte(pizzaDeLaCasa);
// Sorpresa: la PLANTILLA "pizzeria" ara tambe ofereix la pizza de Da Luigi,
// i amb ella tots els restaurants que es donin d'alta a partir d'avui.La regla per decidir la profunditat, camp a camp:
| Tipus de camp | Còpia necessària? |
|---|---|
Primitius (int, boolean...) |
La còpia superficial ja els duplica: res a fer |
Referències a immutables (String, BigDecimal, LocalDate, records immutables, enums) |
Compartir-les és segur i fins i tot desitjable: res a fer |
| Referències a mutables (col·leccions, objectes amb setters) | Còpia profunda obligatòria si el clon ha de ser independent |
Referències a entitats compartides a propòsit (el Restaurant, el Client) |
Compartir és el correcte: clonar aquí seria un error de domini |
Fixa't en la darrera fila: profunditat no significa "clonar-ho tot". En repetir una comanda, el clon ha d'apuntar al mateix client i restaurant (són entitats del món, no parts de la comanda), però necessita la seva pròpia llista de línies. Decidir on aturar la còpia és disseny, no mecànica; la distinció composició/agregació de la lliçó d'UML és exactament el mapa: allò compost (rombe negre) es clona, allò agregat (rombe blanc) es comparteix.
El mecanisme estàndard de Java: Cloneable i els seus problemes
Java porta clonació de sèrie: el mètode protegit Object.clone() i la interfície marcadora Cloneable. El seu aspecte:
public class CartaRestaurant implements Cloneable {
private String nom;
private List<Seccio> seccions = new ArrayList<>();
@Override
public CartaRestaurant clone() {
try {
CartaRestaurant copia = (CartaRestaurant) super.clone(); // copia superficial
// Arranjament manual del mutable per fer-la profunda:
copia.seccions = new ArrayList<>();
for (Seccio s : this.seccions) {
copia.seccions.add(s.clone()); // exigeix que Seccio repeteixi tot aixo
}
return copia;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // impossible: som Cloneable
}
}
}Funciona, però el mecanisme està universalment assenyalat com un dels dissenys fallits del llenguatge (Bloch li dedica un capítol demolidor a Effective Java). Els seus problemes, catalogats:
Cloneableno declaraclone(): és una interfície buida que només canvia el comportament d'un mètodeprotectedheretat d'Object. No pots escriureCloneable c = ...; c.clone();— la interfície no et dona el mètode. Un contracte invisible.super.clone()és superficial sempre: fa còpia binària camp a camp. Tot el mutable queda compartit llevat que ho arreglis a mà, i oblidar un camp és un bug silenciós (l'accident de la carta).CloneNotSupportedExceptionés checked: t'obliga altry/catchritual encara que sigui impossible que salti.- Se salta els constructors: el clon neix sense passar per cap constructor, esquivant invariants, validacions i camps
finalque voldries recalcular. Malíssim casament amb les classes immutables de l'estil Builder. - Fràgil amb l'herència: si una subclasse afegeix camps mutables i oblida redefinir
clone(), hereta una còpia superficial trencada dels seus.
Conclusió pràctica honesta: coneix Cloneable per llegir-lo (apareix en codi antic i al JDK: els arrays i ArrayList l'usen), però no el triïs per a codi nou. El patró Prototype no exigeix Cloneable: exigeix una operació de còpia, i hi ha maneres millors de donar-la-hi.
L'alternativa recomanada: constructors de còpia
Un constructor de còpia és un constructor que rep una altra instància i copia d'ella. Tot explícit, sense excepcions rituals, passant pel constructor (invariants intactes), i compatible amb camps final:
public final class Comanda implements Prototip<Comanda> {
private final Client client; // compartit: entitat
private final Restaurant restaurant; // compartit: entitat
private final List<LiniaComanda> linies; // PROPI: copiar en profunditat
private final Adreca adrecaLliurament; // immutable: compartible
private final String instruccions; // immutable: compartible
// ... resta de camps de la llico anterior ...
/** Constructor de copia: la politica de profunditat, explicita i en un sol lloc. */
private Comanda(Comanda original) {
this.client = original.client; // compartir (agregacio)
this.restaurant = original.restaurant; // compartir (agregacio)
this.linies = original.linies.stream() // aprofundir (composicio)
.map(LiniaComanda::new) // <- copia de cada linia
.collect(Collectors.toCollection(ArrayList::new));
this.adrecaLliurament = original.adrecaLliurament; // immutable: compartir
this.instruccions = original.instruccions;
// ...
}
@Override
public Comanda clonar() {
return new Comanda(this);
}
}
public class LiniaComanda {
private final Producte producte; // entitat de la carta: compartir
private int quantitat; // primitiu mutable: viatja amb la copia
/** Constructor de copia de la linia. */
public LiniaComanda(LiniaComanda original) {
this.producte = original.producte;
this.quantitat = original.quantitat;
}
// ...
}I el cas d'ús queda en dues línies, immune a futurs camps nous (el constructor de còpia viu a la classe, així que qui afegeixi un camp el té davant i l'oblit és molt més difícil que en una còpia externa):
public class ServeiRepetirComanda {
public Comanda repetir(Comanda anterior) {
Comanda nova = anterior.clonar(); // tot l'estat viatja: linies, adreca...
return nova.ambFranja(null) // ajustos del nou context: la franja
.ambDataCreacio(LocalDateTime.now()); // i la data no s'hereten
}
}(Els mètodes ambX(...) —withers— retornen una còpia amb aquell camp canviat: el complement natural de Prototype sobre objectes immutables. Els records de Java els fan trivials.)
Altres alternatives de còpia en Java
- Mètodes de fabricació de còpia:
public static Comanda copiaDe(Comanda c)— idèntic al constructor de còpia amb un nom més expressiu. - Clonar via builder: si la classe ja té Builder, un mètode
toBuilder()que retorna un builder precarregat amb l'estat actual uneix el millor de tots dos patrons:anterior.toBuilder().franja(altraFranja).build(). És la manera més idiomàtica de "copiar amb retocs" en Java modern (Lombok la genera amb@Builder(toBuilder = true)). - Records amb
with-semàntica: per a objectes de valor petits, un record immutable + withers cobreix el 90% de les necessitats de còpia sense patró explícit. - Serialitzar i deserialitzar (a JSON o binari): còpia profunda "automàtica" de tot el graf. Útil en utilitats genèriques de test, però lenta, fràgil davant camps no serialitzables i sense control de la política compartir/copiar: no la usis com a mecanisme de domini.
Registre de prototips
Quan els prototips són un catàleg gestionat —exactament les nostres plantilles de carta—, el patró es completa amb un registre: un contenidor que associa claus a exemplars prototípics i lliura clons (mai l'original) a qui en demani:
public class RegistrePlantillesCarta {
private final Map<String, CartaRestaurant> plantilles = new ConcurrentHashMap<>();
/** Es pobla a l'arrencada... o en calent, sense desplegar codi nou. */
public void registrar(String clau, CartaRestaurant plantilla) {
plantilles.put(clau, plantilla);
}
/** SEMPRE lliura un clon: l'exemplar mestre queda intocable. */
public CartaRestaurant crear(String clau) {
CartaRestaurant plantilla = plantilles.get(clau);
if (plantilla == null) {
throw new IllegalArgumentException("No hi ha plantilla: " + clau);
}
return plantilla.clonar();
}
}
// Arrencada de PideYa:
registre.registrar("pizzeria", cartaPizzeriaBase);
registre.registrar("japones", cartaJaponesBase);
// Alta d'un restaurant: un objecte gran, muntat i llest, sense coneixer classes
CartaRestaurant carta = registre.crear("pizzeria");
carta.setNom("Pizzeria Da Luigi");Compara amb el registre de fàbriques de la lliçó de Factory Method: l'estructura és bessona (un mapa de clau → manera de crear), però allà es registraven receptes (Supplier) i aquí es registren exemplars. La diferència pràctica és que un exemplar es pot configurar amb dades, en temps d'execució (un gestor de continguts podria compondre una plantilla nova des d'un panell d'administració i registrar-la al vol), mentre que una recepta nova exigeix codi nou. Aquest és l'avantatge diferencial de Prototype: noves "classes de facto" sense noves classes.
Quan usar-lo i quan no
Usa'l quan:
- L'objecte nou s'assembla molt a un d'existent i copiar-lo és més simple (i més robust) que reconstruir-lo: "repetir comanda".
- Els exemplars són cars de muntar (base de dades, càlcul) i clonar amortitza el cost: plantilles de carta.
- Les variants de producte es defineixen per configuració d'estat més que per comportament diferent: dotzenes de plantilles no justifiquen dotzenes de classes; un prototip per plantilla, sí.
- El client ha de crear objectes sense conèixer les seves classes concretes i no vols una jerarquia paral·lela de fàbriques.
No l'usis quan:
- Els objectes són petits i barats de construir:
newo un builder són més clars que raonar sobre profunditats de còpia. - El graf de l'objecte té referències circulars o recursos no copiables (connexions, fitxers oberts): clonar es torna un pantà.
- El que varia entre "exemplars" és comportament, no estat: això demana subclasses o Strategy, no clons.
Relació amb altres patrons (només menció): el registre de prototips és un Factory Method/simple factory el mecanisme intern del qual és el clonatge, i una Abstract Factory es pot implementar clonant un joc de prototips per família; Memento usa còpies d'estat amb una altra finalitat (desfer); Composite i Decorator produeixen estructures que sovint convé clonar completes.
Errors Comuns i Consells
- Còpia superficial accidental: l'error número u. Cada camp mutable compartit sense voler és una bomba de rellotgeria que explota lluny del clon (l'accident de la plantilla de carta). Revisa la taula de la secció 3 camp a camp.
- Clonar de més: duplicar el
Restauranten repetir una comanda crearia dues "veritats" sobre la mateixa entitat. La pregunta correcta mai no és "com ho clono tot?" sinó "on acaba aquest objecte?" (composició vs. agregació). - Copiar l'estat que no ha de viatjar: identificadors únics, dates de creació, estat del flux (una comanda JA_LLIURADA clonada no pot néixer lliurada). Defineix al constructor de còpia què es reinicia, no només què es copia.
- Implementar
Cloneable"perquè és l'estàndard": ja has vist el catàleg de trampes. En codi nou: constructor de còpia otoBuilder(). - Registre que lliura l'original: si
crear()retorna la plantilla sense clonar, el primer restaurant que personalitzi la seva carta redecora la plantilla de tothom. El registre clona sempre. - Consell: escriu un test d'independència per cada classe clonable: clona, muta el clon a fons i verifica que l'original no ha canviat (i viceversa). És barat i caça les còpies superficials accidentals abans que producció.
Exercicis
Exercici 1: política de còpia de CartaRestaurant
CartaRestaurant té: String nom, Restaurant propietari, List<Seccio> seccions (i cada Seccio: String titol, List<Producte> productes, on Producte té preu editable per restaurant). Decideix, camp a camp i amb justificació, què es comparteix i què es copia en profunditat en clonar una plantilla per a un restaurant nou, i escriu el constructor de còpia de CartaRestaurant i Seccio.
Exercici 2: caçar el bug de còpia
Aquest codi de "repetir comanda" va sortir a producció. Quin bug té i com es manifesta?
public Comanda repetir(Comanda anterior) {
Comanda nova = new Comanda(anterior.getClient(), anterior.getRestaurant(),
anterior.getLinies(), // <-- atencio aqui
anterior.getAdrecaLliurament());
return nova;
}
// Despres, en el flux de l'app:
nova.getLinies().removeIf(l -> !l.getProducte().estaDisponible());Exercici 3: repetir comanda, complet
Usant la Comanda amb constructor de còpia de la lliçó, implementa ServeiRepetirComanda.repetir(Comanda anterior) complint: (a) les línies el producte de les quals ja no sigui disponible a la carta s'eliminen del clon; (b) la franja horària no s'hereta; (c) si cap línia no sobreviu, llançar ComandaNoRepetibleException.
Solucions
Solució 1: nom — String immutable, es comparteix la referència (i normalment se sobreescriu just després). propietari — entitat agregada; al clon ni tan sols es copia el de la plantilla: s'assigna el nou restaurant (o null fins a l'assignació). seccions — composició pura: còpia profunda. productes dins de cada secció — com que el preu és editable per restaurant, cada carta necessita els seus propis productes: profunditat també aquí (si els productes fossin immutables i el preu visqués fora, compartir-los seria el correcte: la política depèn del disseny del domini, no hi ha resposta mecànica).
private CartaRestaurant(CartaRestaurant original, Restaurant nouPropietari) {
this.nom = original.nom;
this.propietari = nouPropietari;
this.seccions = original.seccions.stream()
.map(Seccio::new)
.collect(Collectors.toCollection(ArrayList::new));
}
public Seccio(Seccio original) {
this.titol = original.titol;
this.productes = original.productes.stream()
.map(Producte::new) // Producte necessita el SEU constructor de copia
.collect(Collectors.toCollection(ArrayList::new));
}Solució 2: anterior.getLinies() passa la mateixa llista a la comanda nova (còpia superficial de facto). El removeIf posterior elimina els productes no disponibles... també de la comanda original: l'historial del client muta retroactivament (la seva comanda del divendres "perd" línies), i si l'original era en pantalla o en cache, mostra dades corruptes. Manifestació típica: incidències intermitents de "la meva comanda antiga ha canviat sola". Correcció: còpia profunda de les línies al constructor (o clonar() de la lliçó).
Solució 3:
public class ServeiRepetirComanda {
public Comanda repetir(Comanda anterior) {
Comanda clon = anterior.clonar(); // copia profunda de linies
clon.getLiniesModificables()
.removeIf(l -> !l.getProducte().estaDisponible()); // (a) nomes afecta el clon
if (clon.getLinies().isEmpty()) {
throw new ComandaNoRepetibleException( // (c)
"Cap producte de la comanda segueix disponible");
}
return clon.ambFranja(null); // (b) la franja no s'hereta
}
}La gràcia és a (a): podem esporgar línies del clon amb tota tranquil·litat perquè la còpia és profunda; amb la versió de l'exercici 2, aquest mateix codi corrompria l'historial.
Conclusió
Prototype completa el repertori creacional amb el seu punt de partida únic: copiar un exemplar en comptes d'instanciar una classe, amb dos casos de PideYa on és la solució natural (repetir comanda, plantilles de carta) i un registre que converteix els exemplars en un catàleg viu, ampliable amb dades i no només amb codi. L'essencial que t'endus és criteri, més que mecànica: la frontera superficial/profunda es decideix camp a camp (compartir entitats, copiar composicions, reiniciar el que no ha de viatjar), Cloneable es llegeix però no s'escriu, i el constructor de còpia —o toBuilder()— és el vehicle sa en Java.
Amb això, els cinc patrons creacionals són sobre la taula: control d'instàncies, fàbriques de productes i de famílies, construcció pas a pas i clonació. Falta el més important: saber triar entre ells, veure com es combinen i repassar què va quedar instal·lat a cada racó de PideYa. És la feina de la darrera lliçó del mòdul: Comparativa i Elecció de Patrons Creacionals.
Curs de Patrons de Disseny de Programari
Mòdul 1: Introducció als Patrons de Disseny
- Què són els Patrons de Disseny?
- Història i Origen dels Patrons de Disseny
- Principis de Disseny: SOLID i Altres Fonaments
- UML Essencial per Entendre Patrons
- Classificació dels Patrons de Disseny
- Avantatges i Desavantatges d'Usar Patrons de Disseny
Mòdul 2: Patrons Creacionals
- Introducció als Patrons Creacionals
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa i Elecció de Patrons Creacionals
Mòdul 3: Patrons Estructurals
- Introducció als Patrons Estructurals
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa i Elecció de Patrons Estructurals
Mòdul 4: Patrons de Comportament
- Introducció als Patrons de Comportament
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa i Elecció de Patrons de Comportament
Mòdul 5: Aplicació de Patrons de Disseny
- Com Seleccionar el Patró Adequat
- Exemples Pràctics d'Ús de Patrons
- Patrons de Disseny en Projectes Reals
- Refactorització Usant Patrons de Disseny
- Antipatrons: Quan els Patrons es Tornen un Problema
Mòdul 6: Patrons de Disseny Avançats
- Patrons de Disseny en Arquitectures Modernes
- Patrons de Disseny en Microserveis
- Patrons de Disseny en Sistemes Distribuïts
- Patrons de Concurrència
- Patrons de Disseny en Desenvolupament Àgil
