Prototype permette di creare nuovi oggetti clonando un oggetto iniziale, detto appunto prototipo. A differenza di altri pattern come Abstract factory o Factory method permette di specificare nuovi oggetti a tempo d'esecuzione (run-time), utilizzando un gestore di prototipi (prototype manager) per salvare e reperire dinamicamente le istanze degli oggetti desiderati.
Prototype è uno dei design pattern fondamentali definiti dalla cosiddetta Gang of Four.
Applicabilità
Come altri pattern creazionali, ovvero che si occupano di istanziare oggetti, prototype mira a rendere indipendente un sistema dal modo in cui i suoi oggetti vengono creati.
Inoltre può rivelarsi utile quando
le classi da istanziare sono specificate solamente a tempo d'esecuzione, per cui un codice statico non può occuparsi della creazione dell'oggetto, oppure
per evitare di costruire una gerarchia di factory in parallelo a una gerarchia di prodotti, come avviene utilizzando Abstract factory e Factory method, oppure
quando le istanze di una classe possono avere soltanto un limitato numero di stati, per cui può essere più conveniente clonare al bisogno il prototipo corrispondente piuttosto che creare l'oggetto e configurarlo ogni volta.
Struttura
Il seguente diagramma delle classi in UML è riferito a un semplice esempio in Java ma è facilmente applicabile a qualsiasi linguaggio orientato agli oggetti, come per esempio il C++.
Diagramma delle classi in UML per il pattern Prototype
Prototype
Prototype definisce un'interfaccia per clonare se stesso.
ConcretePrototype
Le sottoclassi ConcretePrototype implementano l'interfaccia di Prototype, fornendo un'operazione per clonare se stessi.
Client
Client crea un nuovo oggetto del tipo desiderato chiedendo a un prototipo di clonarsi, ovvero invocando il metodo clone definito da ConcretePrototype.
Collaborazioni
Lo schema delle collaborazioni è estremamente semplice: il cliente chiede a un prototipo di clonarsi, ottenendo una copia dell'oggetto desiderato.
Conseguenze
Indipendenza dal metodo d'instanziazione
Come i patternAbstract factory e Builder, Prototype permette di incapsulare al suo interno la modalità di istanziazione degli oggetti, liberando i Client dalla necessità di conoscere i nomi delle classi da instanziare.
Modularità a run-time
Prototype è più flessibile di altri pattern creazionali, perché l'aggiunta di un prodotto richiede semplicemente la registrazione dell'oggetto da clonare in un gestore di prototipi (prototype manager), descritto nella sezione implementazione. Questa caratteristica permette a un Client di aggiungere un prodotto a tempo d'esecuzione, e di renderlo disponibile per la clonazione ad altri Client.
Definire nuovi oggetti modificando valori
Quando si devono definire numerosi oggetti differenziati tra loro solo dai valori che assumono le loro variabili interne è più comodo istanziare nuovi oggetti semplicemente clonando un prototipo iniziale e successivamente impostare la rappresentazione interna perché assuma la configurazione desiderata.
Per esempio un editor di spartiti musicali potrebbe istanziare un solo prototipo di nota, clonarlo e impostare altezza e durata invece di definire una classe per ogni nota.
Definire nuovi oggetti modificando la struttura
Nelle applicazioni che aggregano oggetti diversi in strutture composte, magari utilizzando i patternComposite o Decorator, l'utilizzo di prototipi può semplificare la gestione e la generazione di parti e sottoparti.
Per esempio, un programma di grafica vettoriale potrebbe permettere all'utente di salvare composizioni di oggetti per poi poterne generare copie al bisogno.
Minore necessità di sottoclassi
Il pattern Prototype permette di risolvere un problema di Factory method relativo alla dimensione della gerarchia di classi necessarie. Usando un metodo factory è necessario creare sottoclassi per inserire un nuovo prodotto e, se si hanno numerosi prodotti molto simili tra di loro, la definizione di una nuova classe per ognuno può portare a grandi quantità di codice duplicato.
Usando i prototipi non sono necessarie né una classe factory, né la gerarchia di classi associata ai prodotti: i nuovi oggetti vengono istanziati e inizializzati variando valori interni e struttura, come spiegato precedentemente.
Difficoltà legate alla clonazione
L'unica difficoltà del pattern Prototype potrebbe risiedere nell'implementazione dell'operazione clone. Il metodo clone deve comportarsi come una copia in profondità (deep copy), in modo che la copia di un oggetto composto implichi la copia delle sue sottoparti.
Poiché molti linguaggi di programmazione utilizzando una copia semplice (shallow copy), l'operazione clone deve essere ridefinita dal programmatore e ciò può risultare particolarmente complesso in presenza di strutture dati con riferimenti circolari o nel caso alcuni oggetti non permettano la copia.
Implementazione
L'implementazione del pattern Prototype può comprendere diverse necessità, come l'utilizzo di un gestore di prototipi, la definizione di un'operazione di copia in profondità e l'inizializzazione degli oggetti appena clonati.
Utilizzo di un gestore di prototipi
Se il numero di prototipi utilizzati può variare a tempo d'esecuzione perché i Client possono registrarne di nuovi, è necessario l'utilizzo di un gestore (prototype manager) che si occupi di gestire i prototipi invece dei clienti. Un Client non utilizzarà i prototipi direttamente, ma li salverà e li recupererà utilizzando il gestore.
Il gestore di prototipi utilizza una struttura dati associativa, come per esempio una mappa (map), che permetta di identificare un prototipo a partire da una data chiave (key). Tra le varie operazioni che può definire le più basilari sono l'inserimento di un prototipo associato a una chiave, l'eliminazione di un prototipo a partire dalla chiave data e la possibilità di generare l'elenco delle chiavi memorizzate perché sia consultabile a tempo d'esecuzione.
Implementazione dell'operazione di copia
L'implementazione dell'operazione clone è la parte più delicata del pattern. Nella grande maggioranza dei casi gli oggetti copiati devono essere indipendenti uno dall'altro, l'originale dalla copia. Ciò può portare a problemi nel caso di composizioni di oggetti.
Se gli oggetti sono semplici è sufficiente utilizzare qualche costrutto del linguaggio che permetta la copia di aree di memoria. C++ fornisce un costrutto per la copia, Java definisce un'operazione clone nell'oggetto predefinito Object e qualunque altro oggetto la eredita automaticamente.
Tuttavia, quando un oggetto contiene riferimenti interni ad altri oggetti, copiare l'oggetto contenitore utilizzando una copia semplice non clona anche gli oggetti contenuti, ma solo i loro riferimenti. Questo provoca una situazione di aliasing, ovvero di riferimenti multipli a un oggetto: poiché solo l'oggetto contenitore è stato duplicato, gli oggetti contenuti sono raggiungibili sia dall'originale che dalla copia, rendendo copia e originale non indipendenti tra loro.
Per risolvere questo problema è necessaria una copia in profondità, ovvero un'operazione di clonazione che duplichi l'oggetto su cui è invocata e che chiami le operazioni clone di tutti gli oggetti di cui esiste un riferimento interno, che a loro volta si devono comportare analogamente per garantire la copia di tutta la struttura.
Nel caso di riferimenti circolari è necessario un meccanismo aggiuntivo per evitare di clonare più volte lo stesso oggetto.
Inizializzazione delle copie
Alcuni Client potrebbero avere la necessità di ricevere copie già inizializzate di prototipi, ovvero oggetti già impostati con i valori desiderati.
Poiché l'operazione clone deve avere una signature uniforme, mentre le richieste di inizializzazione dei Client possono richiedere i più disparati parametri, è impossibile sfruttare gli argomenti del metodo di clonazione per specificare lo stato del nuovo oggetto.
Se non si vuole esporre dei metodi di manipolazione dell'oggetto per permettere al Client di impostare direttamente i valori dopo aver ricevuto la copia, è possibile definire un'operazione Initialize ("inizializza") negli oggetti prototipo che riceva gli argomenti necessari per impostare lo stato dell'oggetto dopo la copia.
Esempi
Python
importcopy## Prototype Class#classCookie:def__init__(self,name):self.name=namedefclone(self):returncopy.deepcopy(self)## Concrete Prototypes to clone#classCoconutCookie(Cookie):def__init__(self):Cookie.__init__(self,'Coconut')## Client Class#classCookieMachine:def__init__(self,cookie):self.cookie=cookiedefmake_cookie(self):returnself.cookie.clone()if__name__=='__main__':prot=CoconutCookie()cm=CookieMachine(prot)foriinxrange(10):temp_cookie=cm.make_cookie()
#include<iostream>#include<map>#include<string>usingnamespacestd;enumRECORD_TYPE_en{CAR,BIKE,PERSON};typedefunsignedintu_int32_t;/** * Record is the Prototype */classRecord{public:Record(){}~Record(){}virtualRecord*Clone()=0;virtualvoidPrint()=0;};/** * CarRecord is Concrete Prototype */classCarRecord:publicRecord{private:stringm_oStrCarName;u_int32_tm_ui32ID;public:CarRecord(string_oStrCarName,u_int32_t_ui32ID):Record(),m_oStrCarName(_oStrCarName),m_ui32ID(_ui32ID){}CarRecord(CarRecord&_oCarRecord):Record(){m_oStrCarName=_oCarRecord.m_oStrCarName;m_ui32ID=_oCarRecord.m_ui32ID;}~CarRecord(){}CarRecord*Clone(){returnnewCarRecord(*this);}voidPrint(){cout<<"Car Record"<<endl<<"Name : "<<m_oStrCarName<<endl<<"Number: "<<m_ui32ID<<endl<<endl;}};/** * BikeRecord is the Concrete Prototype */classBikeRecord:publicRecord{private:stringm_oStrBikeName;u_int32_tm_ui32ID;public:BikeRecord(string_oStrBikeName,u_int32_t_ui32ID):Record(),m_oStrBikeName(_oStrBikeName),m_ui32ID(_ui32ID){}BikeRecord(BikeRecord&_oBikeRecord):Record(){m_oStrBikeName=_oBikeRecord.m_oStrBikeName;m_ui32ID=_oBikeRecord.m_ui32ID;}~BikeRecord(){}BikeRecord*Clone(){returnnewBikeRecord(*this);}voidPrint(){cout<<"Bike Record"<<endl<<"Name : "<<m_oStrBikeName<<endl<<"Number: "<<m_ui32ID<<endl<<endl;}};/** * PersonRecord is the Concrete Prototype */classPersonRecord:publicRecord{private:stringm_oStrPersonName;u_int32_tm_ui32Age;public:PersonRecord(string_oStrPersonName,u_int32_t_ui32Age):Record(),m_oStrPersonName(_oStrPersonName),m_ui32Age(_ui32Age){}PersonRecord(PersonRecord&_oPersonRecord):Record(){m_oStrPersonName=_oPersonRecord.m_oStrPersonName;m_ui32Age=_oPersonRecord.m_ui32Age;}~PersonRecord(){}Record*Clone(){returnnewPersonRecord(*this);}voidPrint(){cout<<"Person Record"<<endl<<"Name : "<<m_oStrPersonName<<endl<<"Age : "<<m_ui32Age<<endl<<endl;}};/** * RecordFactory is the client */classRecordFactory{private:map<RECORD_TYPE_en,Record*>m_oMapRecordReference;public:RecordFactory(){m_oMapRecordReference[CAR]=newCarRecord("Ferrari",5050);m_oMapRecordReference[BIKE]=newBikeRecord("Yamaha",2525);m_oMapRecordReference[PERSON]=newPersonRecord("Tom",25);}~RecordFactory(){deletem_oMapRecordReference[CAR];deletem_oMapRecordReference[BIKE];deletem_oMapRecordReference[PERSON];}Record*CreateRecord(RECORD_TYPE_enenType){returnm_oMapRecordReference[enType]->Clone();}};intmain(){RecordFactory*poRecordFactory=newRecordFactory();Record*poRecord;poRecord=poRecordFactory->CreateRecord(CAR);poRecord->Print();deletepoRecord;poRecord=poRecordFactory->CreateRecord(BIKE);poRecord->Print();deletepoRecord;poRecord=poRecordFactory->CreateRecord(PERSON);poRecord->Print();deletepoRecord;deletepoRecordFactory;return0;}
C#
publicenumRecordType{Car,Person}/// <summary>/// Record is the Prototype/// </summary>publicabstractclassRecord{publicabstractRecordClone();}/// <summary>/// PersonRecord is the Concrete Prototype/// </summary>publicclassPersonRecord:Record{stringname;intage;publicoverrideRecordClone(){return(Record)this.MemberwiseClone();// default shallow copy}}/// <summary>/// CarRecord is another Concrete Prototype/// </summary>publicclassCarRecord:Record{stringcarname;Guidid;publicoverrideRecordClone(){CarRecordclone=(CarRecord)this.MemberwiseClone();// default shallow copyclone.id=Guid.NewGuid();// always generate new idreturnclone;}}/// <summary>/// RecordFactory is the client/// </summary>publicclassRecordFactory{privatestaticDictionary<RecordType,Record>_prototypes=newDictionary<RecordType,Record>();/// <summary>/// Constructor/// </summary>publicRecordFactory(){_prototypes.Add(RecordType.Car,newCarRecord());_prototypes.Add(RecordType.Person,newPersonRecord());}/// <summary>/// The Factory method/// </summary>publicRecordCreateRecord(RecordTypetype){return_prototypes[type].Clone();}}
Java
/** Prototype Class **/publicclassCookieimplementsCloneable{publicObjectclone(){try{Cookiecopy=(Cookie)super.clone();//In an actual implementation of this pattern you might now change references to//the expensive to produce parts from the copies that are held inside the prototype.returncopy;}catch(CloneNotSupportedExceptione){e.printStackTrace();returnnull;}}}/** Concrete Prototypes to clone **/publicclassCoconutCookieextendsCookie{}/** Client Class**/publicclassCookieMachine{privateCookiecookie;//could have been a private Cloneable cookie; publicCookieMachine(Cookiecookie){this.cookie=cookie;}publicCookiemakeCookie(){return(Cookie)cookie.clone();}publicObjectclone(){}publicstaticvoidmain(Stringargs[]){CookietempCookie=null;Cookieprot=newCoconutCookie();CookieMachinecm=newCookieMachine(prot);for(inti=0;i<100;i++)tempCookie=cm.makeCookie();}}