Wrapping
Oggi vorrei spendere due parole su come non wrappare una libreria di terza parti (si avete letto bene…).
Quasi tutti i programmatori sanno cos’è un wrapper e perché lo si usa. In due parole un wrapper è un’interfaccia per astrarsi dall’API di una certa libreria di terze parti…quindi per non dipendere da essa e per avere un codebase più uniforme possibile.
Purtroppo delle volte, dalle menti di alcuni programmatori, escono dei wrapper che non servono a nulla ma anzi introducono overhead (performance/memoria) e soprattutto bugs!… vedendone uno di questi, ci ho voluto riflettere e scrivere questo post.
Veniamo ad un esempio concreto: abbiamo una libreria audio di terze parti (ie fmod) che vogliamo usare per il nostro progetto. Pur essendo cross platform vogliamo wrapparla per quello che abbiamo detto prima e perchè la nostra nonnina ci ha sempre detto che <<è sempre meglio wrappare…>>
Ci studiamo la libreria e subito iniziamo il lavoro…dopo qualche settimana ci ritroviamo con il nostro wrapper.
Prima di descrivere il nostro wrapper vi faccio una piccolissima panoramica sulla libreria di fmod: i file audio raw vengono processati da un editor che crea dei banchi di suoni…ogni banco è un progetto. Dentro ogni progetto ci sono degli eventi che sono divisi in gruppi. Ogni evento può essere formato da uno o più suoni. Infine ad ogni evento è associato un canale. [scusatemi per la descrizione più che minimale ma non è il cuore del discorso!]
Ora prendiamo di esempio il wrapper di un channel di fmod:
{
public:
void setVolume(float volume){ impl_->setVolume(volume); }
// other 100 similar methods here... :)
private:
FMOD::Channel* impl_;
}
In sostanza quello che abbiamo fatto è una copia 1:1 dell’interfaccia di fmod. A prima vista questo può sembrare giusto ma per molte API (tra cui fmod) [in realtà quasi tutte] è un approccio completamente sbagliato. Perchè? Semplicemente perchè appunto stiamo ricalcando l’API e non astraendola!.
Quel giorno che volessimo abbandonare fmod dovremmo buttare via tutto perchè difficilmente la nuova libreria che andremo a scegliere avrà la stessa struttura. In sostanza è tutto lavoro sprecato….centinaia se non migliaia di righe di codice da cestinare…purtroppo molto spesso non è solo tempo buttato ma vengono a nascere bugs e quasi sicuramente la nostra astrazione, pur non servendo a nulla, costa in termini di tempo e di spazio. Infatti, nell’esempio non si vede, ma c’è anche il problema della duplicazione delle informazioni…
Qual’é l’alternativa?
L’alternativa è localizzare/centralizzare l’uso della libreria per astrarla da tutto il nostro progetto. Per fare questo molto spesso bastano poche interfacce che espongano le funzioni che usiamo della libreria. Una felice conseguenza di questo è che esponiamo solo quelle funzionalità che effettivamente ci servono potendo quindi wrappare la libreria in modo incrementale…
(ie)
{
public:
bool initSoundDevice();
??? getChannel(const char* const name);
void setChannelVolume(??? channel, float volume);
// and so on...
private:
FMOD::System* fmod_;
};
Lo scopo è contenere fmod dentro il nostro AudioController. Qua sorge il problema più grosso: dobbiamo permettere la comunicazione tra il nostro progetto e fmod senza andare a wrappare le singole classi di fmod… In sostanza il problema è il seguente: cosa deve restituire AudioController::getChannel(const char* const) ? La risposta è un HANDLE (un int32?) che fa da bridge tra il mondo esterno e l’AudioController (quindi fmod). E’ l’AudioController che si tiene al suo interno un mapping tra l’handle e l’FMOD::Channel così da risolverlo quando serve.
{
public:
typedef size_t ChannelHandle; // static_assert(sizeof(size_t) == sizeof(FMOD::Channel*));
bool initSoundDevice();
ChannelHandle getChannel(const char* const name)
{
FMOD::Channel* fmodChannel(...);
return reinterpret_cast<size_t>(fmodChannel);
}
void setChannelVolume(ChannelHandle channel, float volume)
{
reinterpret_cast<FMOD::Channel*>(channel)->setVolume(volume);
}
// and so on...
private:
FMOD::System* fmod_;
};
Concentrandoci sulla NOSTRA API, cioè sulle sole funzionalità di cui necessitiamo, e non su quella di fmod abbiamo un wrapper molto più veloce, snello, meno error prone e soprattutto più facilmente adattabile ad un cambio di libreria!
Ciao e alla prossima
