Esempio di dependency injection (DI) in C++…
Salve gente.
Qualche sera fa, vagando sulla Rete, mi sono imbattuto in uno strano oggetto, il Quaject, che ovviamente mi è piaciuto all’istante.
La particolarità di questo oggetto è che quando viene copiato, vengono copiati non solo i suoi attributi (il suo “stato”) ma anche i suoi metodi. Questo significa che anche i metodi possono essere cambiati e sostituiti dinamicamente a runtime.
Come dice Wikipedia:
As the state of a quaject changes, it replaces the implementations of its methods with appropriate special cases.
Emulare un Quaject in C++ è abbastanza semplice (l’ho fatto
): si tratta di una classe con tanti puntatori a oggetti inizialmente posti a NULL. Ovviamente questi puntatori non sono altro che segnaposto per i soliti functor (già trattati in passato) che verranno assegnati e invocati a runtime.
D’accordo, non è la stessa cosa di un Quaject vero e proprio, però ci si avvicina.
In ogni caso questo tipo di classe/oggetto è un caso abbastanza evidente di “dependency injection” (DI) una perifrasi per dire che porzioni di un oggetto vengono definite/ridefinite a runtime e poi sono liberamente invocabili.
Nota: la DI è alla base anche dell’Inversion of Control (IoC) usata per invertire il “normale” flusso di controllo nelle applicazioni (cfr. post di Omar)
Un esempio concreto
Forse un esempio renderà le cose un po’ più comprensibili.
In questo caso uso la dependency injection per cambiare a runtime il comportamento (behavior) di un oggetto di una classe-”bersaglio” (DITarget).
- creo un oggetto della classe DITarget che riceverà un “comportamento” iniziale, espresso sotto forma di un oggetto interno che effettua la somma di due numeri: in altri termini la sua operazione interna sarà una semplice addizione;
- successivamente inietto nell’oggetto DITarget un nuovo “comportamento”, ossia un nuovo oggetto-operazione che sostituirà l’operazione precedente. In questo caso l’operazione immessa sarà una semplice sottrazione;
- creo un altro oggetto DITarget, copia del primo e che condivide con quest’ultimo il “comportamento”, ossia la sottrazione.
Reference counting
Per evitare eventuali problemi in fase di deallocazione della memoria dovuti al punto 3 (più puntatori in oggetti diversi che puntano tutti allo stesso oggetto-operazione), ho incluso un semplice meccanismo di reference counting (refcount) per tener traccia del numero di puntatori a un oggetto-operazione.
Quando quell’oggetto non è più usato da nessuno, verrà finalmente deallocato secondo la filosofia dell’ultimo chiude la porta.
Nota: date le dimensioni contenute dell’esempio ho preferito non affidarmi alle Boost.
Il codice
Nota: al solito, un sentito ringraziamento a Contezero per il QA Check.
/**
* FILE : DependencyInjectionExample.cpp
* AUTHOR : Gian Paolo "JP" Ghilardi (http://rejex.wordpress.com)
* QA CHECK : Eros "Contezero" Pedrini (http://www.contezero.net/)
* LICENSE : released under the terms of GPL v2.0 ("only")
* COMPILE : g++ -Wall -pedantic DependencyInjectionExample.cpp
* -o DependencyInjectionExample
* PURPOSE : simple example of a Dependency Injection used to change the
* behavior of an object at runtime (C++)
* VERSION : 1.0 (with templates)
*
* TESTED ON: MacOSX 10.5.6 PPC, G++ 4.0.1
*
* REFERENCES:
* [1]: http://en.wikipedia.org/wiki/Dependency_injection
* [2]: http://en.wikipedia.org/wiki/Inversion_of_control
* [3]: http://en.wikipedia.org/wiki/Reference_counting
*/
#include <cstdlib>
#include <iostream>
#include <string>
#include "DependencyInjectionExample.h"
using namespace std;
using namespace di;
/**
* two simple concrete classes extending the IInjectedOp abstract class:
* each one can be used to define or redefine (via Dependency Injection)
* the "behavior" of a DITarget-class object that is its internal
* operation
*/
class Addition: public IInjectedOp
{
public:
Addition(): IInjectedOp(string("ADD")) {}
int operator() (int a, int b) { return a + b; }
};
class Subtraction: public IInjectedOp
{
public:
Subtraction(): IInjectedOp(string("SUB")) {}
int operator() (int a, int b) { return a - b; }
};
/**
* main function: shows dependency injection (DI) used to
* change the behavior of a target-object
*/
int main(int argc, char **argv)
{
cout << "DEPENDENCY INJECTION EXAMPLE:" << endl;
cout << "Simple Dependency Injection example in C++." << endl << endl;
DITarget target(new Addition()); // create a target-object
cout << "add(5,2)=" << target(5, 2) << endl; // now "it's" an addition
target.inject(new Subtraction()); // change the behavior (injection of
// a new internal op)
cout << "sub(5,2)=" << target(5, 2) << endl; // ... and now "it's" a subtraction
DITarget target2(target); // create another target-object with
// the same behavior (internal op)
// of the previous one
cout << "sub(7,1)=" << target2(7, 1) << endl; // the internal op is a subtraction
cout << endl;
return EXIT_SUCCESS;
}
/**
* FILE : DependencyInjectionExample.h
* AUTHOR : Gian Paolo "JP" Ghilardi (http://rejex.wordpress.com)
* QA CHECK : Eros "Contezero" Pedrini (http://www.contezero.net/)
* LICENSE : released under the terms of GPL v2.0 ("only")
* COMPILE : g++ -Wall -pedantic DependencyInjectionExample.cpp
* -o DependencyInjectionExample
* PURPOSE : simple example of a Dependency Injection used to change the
* behavior of an object at runtime (C++)
* VERSION : 1.0 (with templates)
*
* TESTED ON: MacOSX 10.5.6 PPC, G++ 4.0.1
*
* NOTE: for more info, please see file MemoizationExample.cpp
*
*/
#ifndef DEPENDENCYINJECTIONEXAMPLE_H_
#define DEPENDENCYINJECTIONEXAMPLE_H_
#include <cstdlib>
#include <iostream>
#include <string>
#include <exception>
//#define DEBUG // uncomment to enable refcount debug messages
namespace di
{
using namespace std;
/**
* This abstract class acts as a place-holder for its derived
* classes in DITarget class.
*
* It contains also a simple reference counting mechanism:
* for this purpose it acts as a mixin/trait for its derived classes
*/
class IInjectedOp
{
private:
string name_;
int refcount_;
public:
IInjectedOp(string name)
{
refcount_ = 1; // first use
name_ = name;
#ifdef DEBUG
cout << "[NEW] name='" << name_ << "', refcount="
<< refcount_ << endl;
#endif
}
virtual ~IInjectedOp() {}
virtual int operator() (int a, int b) = 0;
inline string getName() { return name_; }
inline int getRefs() { return refcount_; }
inline void incRefs() { refcount_++; };
inline void decRefs() { refcount_--; };
};
/**
* example of class subject
* to dependency injection
*/
class DITarget
{
private:
IInjectedOp *op_; // internal operation (subjected to injection)
// try to clear the internal op pointer
inline void disposeOp()
{
op_->decRefs(); // decrement current op refcount
// check if we hold the last pointer to the object...
if(op_ != NULL && op_->getRefs() == 0)
{
#ifdef DEBUG
cout << "[CLEAN] name='" << op_->getName() << "', refcount="
<< op_->getRefs() << " (obj deleted)" << endl;
#endif
delete op_;
}
op_ = NULL;
}
public:
// "standard" constructor, used to save the internal operation (functor)
inline DITarget(IInjectedOp *startOp)
{
// NOTE: refcount already incremented by IInjectedOp ctor
op_ = startOp;
}
// copy constructor
inline DITarget(DITarget &toCopy)
{
op_ = toCopy.op_; // copy the pointer
op_->incRefs(); // new pointer use => refcount increment
#ifdef DEBUG
cout << "[CPCTOR] name='" << op_->getName() << "', refcount="
<< op_->getRefs() << endl;
#endif
}
// simple destructor
~DITarget()
{
#ifdef DEBUG
cout << "[DTOR] name='" << op_->getName() << "', refcount="
<< op_->getRefs() << endl;
#endif
disposeOp();
}
// used to perform the operation: the actual operation
// is delegated to the internal operation object (functor)
inline int operator() (int a, int b) { return (*op_)(a, b); } //
/**
* dependency injection function: used to change
* the internal operation of this class
* @param toInject pointer to an object of a class
* extending IInjectedOp interface.
*/
inline void inject(IInjectedOp *toInject)
{
disposeOp(); // try a cleanup of the current op
op_ = toInject; // save the new op (dependency injection)
}
};
}
#endif /* DEPENDENCYINJECTIONEXAMPLE_H_ */
L’esempio dal vivo
L’esempio in esecuzione (rispettivamente senza e con i messaggi di debug per il refcount).


delegate int Target(int a, int b); public void main() { Target Addition = delegate(int a, int b) { return a + b; }; Target Subtraction = delegate(int a, int b) { return a - b; }; Target target = Addition; // create a target-object Console.WriteLine( "add(5,2)=" + target(5, 2) + "\n"); // now "it's" an addition target = Subtraction; Console.WriteLine("sub(5,2)=" + target(5, 2) + "\n"); // now "it's" a substractior }[ho approvato io il commento perché il padrone di casa è assente
]
Premetto che l’esempio era un po’ diverso e scritto in maniera volutamente e fortemente prolissa nonchè “didattico-didascalica”.
I delegate sono spettacolari e ne abuso con giubilo quando posso. Ne apprezzo soprattutto lo stile compatto+agevole e trovo fantastico poter scrivere direttamente qualcosa come appunto:
Target Addition = delegate(int a, int b) { return a + b; };Quella che riporto di seguito è una possibile versione corrispondente al tuo codice in C++ standard (parlo di quello attuale: con i variadic template e le tuple del nuovo sarà possibile ottenere functor molto più versatili)
/* * REFERENCE: * [http://www.cplusplus.com/reference/std/functional/binary_function.html] * * Uso le struct perchè rispetto alle classi il comportamento è "public" * per default. Questo mi fa anche risparmiare la keyword "public" quando * eredito da Target. ^_^ */ #include <cstdlib> #include <iostream> using namespace std; struct Target: public binary_function<int, int, int> { virtual int operator() (const int &, const int &) = 0; virtual ~Target() {} }; struct Addition: Target { int operator() (const int &a, const int &b) { return a+b; } }; struct Subtraction: Target { int operator() (const int &a, const int &b) { return a-b; } }; int main(int argc, char** argv) { Target *target = new Addition(); cout << "add(5,2)=" << (*target)(5, 2) << endl; // now “it’s” an addition delete target; target = new Subtraction(); cout << "sub(5,2)=" << (*target)(5, 2) << endl; // now “it’s” a subtraction delete target; // for easy memory deallocation, try smart pointers. ^_^ return EXIT_SUCCESS; }Torniamo al solito problema: il C# è focalizzato a rendere tutto facilissimo per l’utente mentre il C++ è più prolisso e formale (IMHO, ovviamente).
In questo caso il C# tratta i delegate direttamente come dei tipi e risparmia all’utilizzatore la fatica di dover creare classi da usare come functor. Inoltre permette di inizializzarli come funzioni dentro una funzione. Poi in C# tutto è un reference (come in Java), quindi niente puntatori fra i piedi e deallocazione automatica/automatizzata della memoria via Garbage Collection.
Infine, anche voledo, non si possono usare i reference del C++ allo stesso modo del C# perchè sono un po’ più limitati di quelli della controparte. Quotando Wikipedia:
Ciau!
PS: mi sono permesso di mettere in una box il tuo codice per renderlo più leggibile. Se ho fatto male, chiedo venia. ^^’
Dimmi se ti piace di più (ovviamente è uno scherzo, però funziona)…
#include <cstdlib> #include <iostream> using namespace std; struct Target: public binary_function<int, int, int> { virtual int operator() (const int &, const int &) = 0; virtual ~Target() {} }; #define delegate_decl(strname, op) \ struct strname: Target \ { \ int operator() (const int &a, const int &b) { return (op); } \ } #define delegate(strname) \ strname() int main(int argc, char** argv) { delegate_decl(Addition, a+b); delegate_decl(Subtraction, a-b); Target *target = new delegate(Addition); cout << "add(5,2)=" << (*target)(5, 2) << endl; // now “it’s” an addition delete target; target = new delegate(Subtraction); cout << "sub(5,2)=" << (*target)(5, 2) << endl; // now “it’s” a subtraction delete target; // for easy memory deallocation, try smart pointers. ^_^ return EXIT_SUCCESS; }