risorse | mix-in
I mix-in sono delle estensioni funzionali applicabili a qualunque tipo di oggetto. Esempi di funzionalità che può essere desiderabile aver disponibili indipendentemente dal tipo di oggetto sono la gestione del valor nullo, il conteggio del numero di istanze create o attualmente disponibili, l'istante di creazione dell'istanza, …
Il nome mix-in deriva da una pratica ideata dal gelataio Steve Herrell negli anni '70 che permetteva ai suoi clienti di ricoprire il gelato con uno o più ingredienti a scelta (i mix-in appunto), tipo uvetta, scaglie di cioccolato, mandorle tritate e altro. Così come la presenza dell'ingrediente aggiuntivo non altera la natura del gelato, analogamente l'applicazione del mix-in all'oggetto C++ non ne altera il tipo. Inoltre, così come un cliente non può ottenere un ingrediente aggiuntivo se non a corredo di un gelato, i mix-in vengono utilizzati per estendere una classe esistente: l'istanziazione di un mix-in non è prevista.
La classica mplementazione del mix-in in C++ è la seguente:
template<class T> class Mixin : public T { // mix-in body };
Nell'intento di non alterare il tipo dell'oggetto esteso, le funzionalità aggiuntive vengono implementate dalla classe derivata: questo idioma, tipico del C++, prende il nome di abstract subclass. Per sua natura, il mix-in in C++ non può essere applicato ai tipi base. Segue un esempio di mix-in che assegna ad ogni istanza un identificativo univoco:
#include <string> #include <iostream> template <class T> class SerialNumbered : public T { long serialNumber; static long counter; public: SerialNumbered() { serialNumber = ++counter; } long getSerialNumber() { return serialNumber; } }; template <class T> long SerialNumbered<T>::counter = 0; int main() { typedef SerialNumbered<std::string> StringWithSerialNumber; StringWithSerialNumber aString; // calling a std::string method aString.append("test string 1"); // calling a method that accepts a std::string std::cout << aString << ": #" << aString.getSerialNumber() << std::endl; StringWithSerialNumber anotherString; anotherString.append("test string 2"); std::cout << anotherString << ": #" << anotherString.getSerialNumber() << std::endl; return 0; } /* output: * * test string 1: #1 * test string 2: #2 */
I mix-in non sono facilmente applicabili alle classi prive del costruttore di default; si consideri l'esempio seguente, che provoca un errore di compilazione:
int main() { typedef SerialNumbered<std::string> StringWithSerialNumber; StringWithSerialNumber aString("does not compile"); }
L'errore è dovuto al fatto che la classe SerialNumbered non prevede un costruttore a partire da un array di caratteri; basta tuttavia definire un nuovo costruttore che accetta un const char* e lo inoltra alla classe base std::string per risolvere il problema:
template <class T> class SerialNumbered : public T { long serialNumber; static long counter; void assignSerialNumber() { serialNumber = ++counter; } public: SerialNumbered() { assignSerialNumber(); } SerialNumbered(const char* str) : T(str) { assignSerialNumber(); } long getSerialNumber() { return serialNumber; } };
Questa tecnica, per quanto giustificabile in particolari contesti, non brilla certo per generalità: non è infatti accettabile che il mix-in dipenda dalla classe cui viene applicato. Una soluzione più generale, ma comunque sempre parziale, consiste nel fornire il mix-in di un costruttore basato sul costruttore di copia della classe base:
template <class T> class SerialNumbered : public T { long serialNumber; static long counter; void assignSerialNumber() { serialNumber = ++counter; } public: SerialNumbered() { assignSerialNumber(); } SerialNumbered(const T& t) : T(t) { assignSerialNumber(); } long getSerialNumber() { return serialNumber; } };
Il mix-in così modificato può essere istanziato a partire da un'istanza della classe base o da qualunque parametro accettato da un conversion constructor della classe base stessa. Una trattazione più approfondita della tematica dei costruttori parametrici si trova in [5].
Aggiornamento [05/09/2013]: il problema dei costruttori è risolvibile per mezzo della constructor inheritance introdotta nel C++11. Un esempio del suo uso in questo contesto è disponibile in [7].
Secondo Bruce Eckel [3], ciò che caratterizza i mix-in è la capacità di essere facilmente ricombinati assieme, e di mantenere allo stesso tempo la proprià identità, di essere cioè perfettamente riconoscibili nell'amalgama finale. Sempre secondo Eckel, il mix-in è strettamente correlato al pattern Decorator, con la sostanziale differenza di non richiedere la struttura gerarchica tipica del Decorator: mentre l'oggetto risultante dalla composizione dei mix-in comprende tutti i tipi primitivi, nel secondo caso il tipo dell'oggetto risultante coincide con quello dell'ultimo Decorator applicato.
Un esempio circa la componibilità dei mix-in è fornita dallo stesso Eckel e qui riportata in una versione leggermente riadattata per il Visual Studio 2008:
#include <ctime> #include <iostream> #include <string> template<class T> class TimeStamped : public T { long timeStamp; public: TimeStamped() { timeStamp = static_cast<long>(time(0)); } long getStamp() { return timeStamp; } }; template<class T> class SerialNumbered : public T { long serialNumber; static long counter; public: SerialNumbered() { serialNumber = counter++; } long getSerialNumber() { return serialNumber; } }; template<class T> long SerialNumbered<T>::counter = 1; int main() { TimeStamped<SerialNumbered<std::string>> mixin1, mixin2; mixin1.append("test string 1"); // A string method mixin2.append("test string 2"); std::cout << mixin1 << " " << mixin1.getStamp() << " " << mixin1.getSerialNumber() << std::endl; std::cout << mixin2 << " " << mixin2.getStamp() << " " << mixin2.getSerialNumber() << std::endl; return 0; } /* output: * * test string 1 1311344893 1 * test string 2 1311344893 2 */
Come si può notare, gli oggetti mixin1 e mixin2 mantengono la loro natura di std::string, pur presentando le nuove interfacce TimeStamped e SerialNumbered. Si nota, inoltre, che l'ordine di applicazione dei mix-in è ininfluente, fintanto che questi sono ortogonali e immuni dal problema del costruttore.
Generalmente, i mix-in hanno lo scopo di implementare un'estensione indipendente dal tipo di oggetto al quale verrà applicata, mentre il CRTP si usa per adattare un'interfaccia e fornire un'implementazione che dipende dall'estensione. Dal punto di vista implementativo, la differenza tra mix-in e CRTP è evidente:
// mix-in template<class T> class Mixin : public T { // mix-in body }; // CRTP template<class T> class Base { }; class Derived : public Base<Derived> { // crtp implementation };
È tuttavia possibile implementare un mix-in per mezzo del CRTP attraverso una derivazione (tant'è che a volte il CRTP viene anche indicato col termine mix-in dall'alto):
#include <ctime> #include <iostream> #include <string> template<class T> class TimeStamped { time_t timeStamp; public: TimeStamped() { timeStamp = static_cast<long>(time(0)); } time_t getStamp() { return timeStamp; } }; class TimeStampedString : public std::string, public TimeStamped<TimeStampedString> { }; int main() { TimeStampedString mixin1, mixin2; mixin1.append("test string 1"); mixin2.append("test string 2"); std::cout << mixin1 << " " << mixin1.getStamp() << std::endl; std::cout << mixin2 << " " << mixin2.getStamp() << std::endl; return 0; } /* output: * * test string 1 1317655246 * test string 2 1317655246 */
Dall'esempio si nota come il CRTP, nel caso di decorazione di una classe derivata, necessita della derivazione multipla. Più complicata risulta l'applicazione di una moltitudine di mix-in CRTP alla stessa classe; la combinazione va infatti resa per mezzo di una catena di derivazione, oppure attraverso una derivazione multipla. L'esempio che segue fa uso di una catena di derivazione:
template<class T> class SerialNumbered { long serialNumber; static long counter; public: SerialNumbered() { serialNumber = counter++; } long getSerialNumber() { return serialNumber; } }; template<class T> long SerialNumbered<T>::counter = 1; class SerialNumberedTimeStampedString : public TimeStampedString, public SerialNumbered<SerialNumberedTimeStampedString> { }; int main() { SerialNumberedTimeStampedString mixin1, mixin2; mixin1.append("test string 1"); mixin2.append("test string 2"); std::cout << mixin1 << " " << mixin1.getStamp() << " " << mixin1.getSerialNumber() << std::endl; std::cout << mixin2 << " " << mixin2.getStamp() << " " << mixin2.getSerialNumber() << std::endl; return 0; } /* output: * * test string 1 1317655550 1 * test string 2 1317655550 2 */
SerialNumberedTimeStampedString può essere definita senza fare esplicito riferimento a TimeStampedString ricorrendo alla derivazione multipla:
class SerialNumberedTimeStampedString : public std::string, public TimeStamped<SerialNumberedTimeStampedString>, public SerialNumbered<SerialNumberedTimeStampedString> { };
Resta aperto il problema dei costruttori, che risulta amplificato nel caso della catena di derivazione, dato che il passaggio dei parametri deve avvenire dal fondo della gerarchia fino ai livelli di competenza.
Le policies [1] possono essere utilizzate per rimandare all'istante dell'istanziazione della classe la scelta dell'incarnazione del mix-in da utilizzare nel caso specifico. Come nel caso del CRTP, le funzionalità del mix-in sono iniettate nella classe da decorare dall'alto, per mezzo dell'ereditarietà:
#include <string> #include <iostream> /* the "serial number" policy implicitly defines the following interface: * * long getSerialNumber() */ template <class SerialNumberPolicy> class StringWithSerialNumberPolicy : public std::string, public SerialNumberPolicy { }; class IncrementalSerialNumber { long serialNumber; static long counter; public: IncrementalSerialNumber() { serialNumber = ++counter; } long getSerialNumber() { return serialNumber; } }; long IncrementalSerialNumber::counter = 0; int main() { typedef StringWithSerialNumberPolicy<IncrementalSerialNumber> StringWithSerialNumber; StringWithSerialNumber aString; // calling a std::string method aString.append("test string 1"); // calling a method that accepts a std::string std::cout << aString << ": #" << aString.getSerialNumber() << std::endl; StringWithSerialNumber anotherString; anotherString.append("test string 2"); std::cout << anotherString << ": #" << anotherString.getSerialNumber() << std::endl; return 0; } /* output: * * test string 1: #1 * test string 1: #2 */
Qualora si renda necessario modificare il comportamento della policy SerialNumberPolicy per una particolare classe, sarà sufficiente introdurre una nuova policy class che implementi il nuovo funzionamento, quindi specificarne il nome all'atto della dichiarazione dell'istanza della classe decorata, senza operare nessun altro intervento sul codice esistente:
class AddressBasedSerialNumber { public: long getSerialNumber() { return reinterpret_cast<long>(this); } }; int main() { typedef StringWithSerialNumberPolicy<AddressBasedSerialNumber> StringWithSerialNumber; StringWithSerialNumber aString; ... } /* output: * * test string 1: #3340532 * test string 1: #3340492 */
Un modo alternativo di decorare la host class rispetto alla derivazione è quello per contenimento; in questo caso, tuttavia, si deve avere l'accortezza di replicare l'interfaccia della policy nella classe decorata:
template <class SerialNumberPolicy> class StringWithSerialNumberPolicy : public std::string { SerialNumberPolicy serialNumberPolicy; public: long getSerialNumber() { return serialNumberPolicy.getSerialNumber(); } };
Pagina modificata il 5/9/2013