Inversion of control e Dependency Injection

in C#, Programmazione

Introduzione alla dependency injection

Dependency Injection (DI) e Inversion Of Control (IoC) sono due elementi fondamentali per lo sviluppo di applicazioni “moderne”. A prima vista possono sembrare due concetti complessi ma, una volta appresi i principi, difficilmente si potrà farne a meno.

Il problema da risolvere è la dipendenza tra oggetti

Analizziamo il seguente codice:

var generator = new Generator();
var engine = new Engine(generator);
var car = new Car(engine);

dove Engine e Car accettono come parametro del loro costruttore un oggetto di tipo Generator. Il codice è perfettamente funzionante, anche se i singoli construttori dipendono dall’implementazione specifica dell’oggetto Generator.

Dal codice precedente possiamo estrarre l’interfaccia per la classe Engine:

public interface IEngine
{
    void Start();
}

e definire la classe facendola ereditare dall’interfaccia stessa:

public class Engine : IEngine
{
    public void Start();
}

L’utilizzo dell’interfaccia è fondamentale perchè consente di generare oggetti che implementeranno tutti con la stessa firma. L’utilizzo dell’interfaccia consente di definire una sorta di “contratto” con tutte le classi che la implementeranno.

A questo punto la classe Car può essere modificata nel seguente modo:

public class Car
{
    private readonly IEngine _engine;

    public void Car(IEngine engine)
    {
        _engine = engine;
    }
}

A questo punto la classe Car accetterà in ingresso oggetti che implementano la classe IEngine, senza fornire il tipo specifico.

Nell’ottica di centralizzare la risoluzione dell’oggetto da istanziare viene utilizzato l’IoC. Possiamo considerarlo (semplificando le sue funzionalità) come un meccanismo nel quale effettuare la registrazione delle corrispondenze tra interfacce e tipi che le implementano. Ad esempio:

Ogni volta che l’interfaccia IEngine viene richiesta, dovrà essere sostituita da Engine. Utilizzando ad esempio Autofac (uno dei tanti IoC disponibili) la sintassi che dovrà essere utilizzata è la seguente:

var builder = new ContainerBuilder();
builder.RegisterType<Generator>().As<IGenerator>();
builder.RegisterType<Engine>().As<IEngine>();
builder.RegisterType<Car>().As<ICar>();
var container = builder.Build();

Dopo aver creato l’oggetto ContainerBuilder viene eseguita la registrazione delle corrispondenze tra tipi. Successivamente viene creato il container (con le rules indicate in precedenza).

E’ possibile istanziare manualmente l’oggetto associato all’interfaccia utilizzando:

var car = resolver.Resolve<ICar>();
car.Start();

I vantaggi

  • Testabilità del codice: utilizzando le interfacce risulta piuttosto semplice creare implementazioni di oggetti “mock” per testare il codice
  • Disaccopiamento: le classi sono disaccopiate, secondo il principio che ogni classe dovrebbe eseguire un singolo compito ed essere il più “isolata” possibile

L’utilizzo di Dependency Injection (DI) e Inversion Of Control (IoC) avviene solitamente in ambienti più complessi di quello descritto in precedenza.