Questo è il primo post di una serie dove proverò ad analizzare come progetti più o meno famosi sono stati implementati.

Parto con Funq, che come descritto dall’autore

“provides a high performance DI framework by eliminating all runtime reflection through the use of lambdas and generic functions as factories.
Developed entirely in C# using TDD, it's a container you can crack open and actually understand to the core.”

Ho colto l’invito e l’ho crack opened. La prima cosa che si nota è quanto la libreria sia compatta e minimalista, meno di 15 files in tutto, di cui:

  • solo 2 classi pubbliche: Container che è l’unico entry point in tutta la libreria e ResolutionException una custom exception
  • alcune fluent interfaces
  • 3 delegate Func aggiuntivi che accettano rispettivamente 5,6 e 7 parametri.
  • 2 classici enums: Owner e ReuseScope che si spiegano da soli
  • un buon numero di delegates dedicati completamente al CompactFramework 2.0 (vedi direttiva “#if CF20”)

Il progetto di test oltre ad essere completo, rappresenta un’ottima risorsa per capire come utilizzare Funq. Per esempio, per registrare e risolvere un’instanza, bastano 3 banali e IMO intuitive righe di codice:

var container = new Container();
container.Register<IFoo>(c => new Foo());
var foo = container.Resolve<IFoo>();

Per risolvere dipendenze:

var container = new Container();
container.Register(c => new Presenter(c.Resolve<View>()));
container.Register(c => new View())
.InitializedBy((c, v) => v.Presenter = c.Resolve<Presenter>());

var view = container.Resolve<View>();
var presenter = container.Resolve<Presenter>();

Assert.AreSame(view.Presenter, presenter);

Veniamo ora ad alcuni punti interessanti. Come già detto il core si trova nella classe Container. Solo osservando i 4 private fields della classe si capiscono molte cose:

Dictionary<ServiceKey, ServiceEntry> services = new Dictionary<ServiceKey, ServiceEntry>();
// Disposable components include factory-scoped instances that we don't keep
// a strong reference to.
Stack<WeakReference> disposables = new Stack<WeakReference>();
// We always hold a strong reference to child containers.
Stack<Container> childContainers = new Stack<Container>();
Container parent;
  1. Per mantenere le istanze dei servizi registrati viene usato un Dictionary.
  2. La libreria supporta gerarchie di container.
  3. Intelligente l’utilizzo di uno Stack di WeakReferences per servizi che implementano IDisposable e vengono completamente gestiti dal container. Questo test ne dimostra l’utilità:
[TestMethod]
public void ContainerOwnedNonReuseInstacesAreNotKeptAlive()
{
var container = new Container();
container.Register<IFoo>(c => new Disposable())
.ReusedWithin(ReuseScope.None)
.OwnedBy(Owner.Container);

var foo = container.Resolve<IFoo>() as Disposable;
var wr = new WeakReference(foo);

foo = null;

GC.Collect();

Assert.IsFalse(wr.IsAlive);
}

Sia per registrare che per risolvere servizi, sono disponibile overloads che accettano costruttori (factory delegate o lambda) aventi da 0 a 6 parametri. Sono disponibili anche LazyResolve overloads che invece di ritornare l’istanza del servizio richiesto, restituiscono un Func delegate ad esso, così che la reale chiamata a Resolve sia ritardata fino a quando il delegate non viene eseguito:

public Func<TArg, TService> LazyResolve<TService, TArg>()
{
return LazyResolve<TService, TArg>(null);
}

Nulla di trascendentale nelle implementazioni interne di Register e Resolve. Register crea un ServiceEntry wrapper attorno al servizio che si sta registrando e lo aggiunge al dictionary. Resolve al contrario, recupera la entry dal dictionary e inizializza l’istanza se necessario (da notare che GetEntry varia a seconda del lifecyle e ownership dell’istanza richiesta):

private TService ResolveImpl<TService>(string name, bool throwIfMissing)
{
// Would throw if missing as appropriate.
var entry = GetEntry<TService, Func<Container, TService>>(name, throwIfMissing);
// Return default if not registered and didn't throw above.
if (entry == null)
return default(TService);

TService instance = entry.Instance;
if (instance == null)
{
instance = entry.Factory(entry.Container);
entry.InitializeInstance(instance);
}

return instance;
}

Degni di nota sono proprio i delegate Initializer e Factory sulla classe ServiceEntry che (oltre al lazy resolve esplicito di cui sopra) permettono anche un lazy resolve implicito, nel senso che un servizio registrato come nell’esempio seguente viene sempre comunque inizializzato solo al momento del resolve e non in fase di registrazione:

var container = new Container();
container.Register<Bar>(c => new Bar());
Assert.IsNotNull(container.Resolve<Bar>()); // Bar constructor executes here

Curiosità

E’ curioso vedere l’implementazione di Equals e GetHashCode su ServiceKey sia cambiata 3 volte in 2 mesi…been there done that…:-)

Varianti

Da Funq sono nati Munq e TinyMunq, inizialmente solo per apportare alcuni miglioramenti in termini di performance. Ora sono progetti indipendenti. Alcuni di queste modifiche degne di nota sono:

  • l’uso di un HybridDictionary invece di un Dictionary per mantenere le istanze dei servizi
  • l’uso di un null check (entry = (ServiceEntry)c.services[key]) == null ) invece di TryGetValue (container.services.TryGetValue(key, out entry)) durante la risoluzione delle istanze
  • il non utilizzo di metodi generici in alcuni frangenti
  • il non utilizzo di chiamate a one liner methods quando possibile

Il codice sorgente contiene un progetto di benchmarking per diversi DI e pare che Munq sia molto più performante di Funq e pare che le modifiche elencate sopra siano responsabili di tale differenza.

Conclusioni

A prescindere dal fatto che questo DI container sia meglio o peggio di altri, è sicuramente facile da capire e devo dire che è stato un piacere analizzarne l’approccio minimalista e assolutamente privo di tecniche di reflection. 

 

P.S.: Daniel Cazzulino ha una interessante serie di video sull’approccio TDD utilizzato per sviluppare Funq.