Usare un contenitore IoC per inserire automaticamente le dipendenze

Completato

L'ultimo schema che verrà esaminato è quello dell'inserimento delle dipendenze.

Lo schema del localizzatore di servizi funziona perfettamente per i progetti più piccoli. Tuttavia può risultare difficile da gestire quando occorre riutilizzare elementi tra varie applicazioni o quando si hanno molti servizi interconnessi. I localizzatori di servizi tendono a essere fragili, perché le dipendenze mancanti non vengono rilevate finché non vengono richieste, ossia nella fase di runtime.

Inserimento delle dipendenze

L'inserimento delle dipendenze (Dependency Injection, DI) è uno schema progettuale che consente di creare codice ad accoppiamento debole. Sia lo schema Factory che quello del localizzatore di servizi consentono di creare classi in una certa misura disaccoppiate dalle relative dipendenze. Tenere presente che le classi non hanno informazioni su come creare un'istanza delle dipendenze. Inoltre, le classi devono accettare dipendenze aggiuntive da una factory o da un localizzatore di servizi per individuare le dipendenze necessarie o crearne un'istanza.

Tuttavia, entrambi gli schemi nascondono le dipendenze. Lo schema DI si basa su tre strategie per rendere individuabili le dipendenze di una classe. Verranno ora esaminate le tre opzioni.

Usare l'inserimento del costruttore

La prima opzione consiste nell'usare l'inserimento del costruttore. Con l'inserimento del costruttore, ci si basa sul contenitore per creare le dipendenze specificate nel costruttore. Si crea quindi un'istanza dell'oggetto con le dipendenze passate.

Si supponga di aver creato una classe DataAccessLayer per gestire le interazioni di livello elevato con un repository di dati. Nell'esempio, la classe DataAccessLayer ha una dipendenza da un repository e dalla finestra di dialogo personalizzata. Si decide di non usare gli schemi Factory o del localizzatore di servizi, perché entrambi nascondono le dipendenze necessarie. Si vuole segnalare a chiunque implementi la classe che questa non può funzionare senza le dipendenze. Il modo più semplice per evidenziare le dipendenze per questa classe è elencarle come parametri del costruttore.

L'aspetto della classe DataAccessLayer potrebbe essere simile al seguente.

public class DataAccessLayer
{
    public DataAccessLayer(
        IDbRepository db,
        IMessageDialog message)
    {
        ...
    }
    ...
}

Sarà innanzitutto necessario creare le due dipendenze richieste e quindi impostarle come parametri del costruttore prima di creare l'istanza dell'oggetto DataAccessLayer.

Usare l'inserimento delle proprietà

La seconda opzione è usare l'inserimento delle proprietà. Si userà l'inserimento delle proprietà se la classe ha una dipendenza facoltativa.

Si supponga che la classe includa una proprietà logger che consente al livello dati di registrare o creare report sulle interazioni dei dati. In questo caso, la funzionalità logger è facoltativa e può essere esclusa in base alle esigenze.

public class DataAccessLayer
{
    public DataAccessLayer(
        IDbRepository db,
        IMessageDialog message)
    {
        ...
    }

    public ILogger Logger {get; set;}
    ...
}

Usare l'inserimento del parametro di metodo

La terza opzione è usare l'inserimento del parametro di metodo. Si userà l'inserimento del parametro di metodo quando solo un singolo metodo di una classe deve avere accesso a una dipendenza.

Si supponga che la classe includa un metodo logger. In questo caso, solo il metodo richiederà l'accesso alla funzionalità logger.

public class DataAccessLayer
{
    public DataAccessLayer(
        IDbRepository db,
        IMessageDialog message)
    {
        ...
    }

    public void Log (ILogger logger)
    {
        logger.LogStatus();
    }
}

Uso dell'inserimento delle dipendenze

Lo svantaggio dell'inserimento delle dipendenze è il fatto che la responsabilità di identificare le dipendenze è stata spostata sul chiamante o sul creatore dell'oggetto.

Ad esempio, per creare l'oggetto DataAccessLayer saranno necessarie le implementazioni concrete di ognuna delle astrazioni.

public DataAccessLayer CreateDataLayer()
{
    var dataAccessLayer = new DataAccessLayer(
        new SqliteRepository_iOS(),                               // IDbRepository
        new MessageDialog_iOS());                                 // IAlertService
        dataAccessLayer.Logger = new AzureLogger(AzureToken);     // ILogger

   return dataAccessLayer
}

Queste implementazioni sono specifiche della piattaforma e sono note solo al codice di piattaforma. Per rendere note queste dipendenze al codice condiviso si può procedere in due modi:

  • Usare una delle due tecniche già illustrate.
  • Spostare la creazione dell'oggetto DataAccessLayer nel codice specifico della piattaforma, come nell'esempio precedente.

Tuttavia, nessuna di queste soluzioni consente di creare codice ad accoppiamento debole. L'obiettivo è ridurre al minimo le informazioni di una classe rispetto alle proprie dipendenze.

Quando si usa l'inserimento delle dipendenze, ci si basa su un'altra classe denominata container. Il contenitore crea gli oggetti e fornisce automaticamente le dipendenze note. In questo modo, si registrano tutte le dipendenze in un'unica posizione. Il codice client non deve conoscere direttamente la provenienza della dipendenza.

Contenitori di inversione del controllo

Il contenitore di inversione del controllo (Inversion of Control, IoC) è un gestore delle dipendenze. Ha due scopi specifici:

  • Opera come registro delle dipendenze. Come per il localizzatore di servizi, si registrano astrazioni e servizi con il contenitore. Internamente, il contenitore IoC usa un localizzatore di servizi per conoscere le eventuali dipendenze che l'applicazione potrebbe usare.

  • Individua le dipendenze del client. Questa funzione è la principale differenza tra il contenitore IoC e il localizzatore di servizi. Quando si usa il localizzatore di servizi, è il client a richiedere le dipendenze. Quando si usa il contenitore IoC, è il contenitore a creare tutti gli oggetti e a inserire le dipendenze come proprietà o parametri del costruttore.

Per creare un oggetto, il contenitore IoC esamina il tipo dell'oggetto e richiama uno dei suoi costruttori. Quindi, passa al costruttore le dipendenze necessarie in base alle astrazioni registrate note. Se queste dipendenze richiedono oggetti, il costruttore può crearli come necessario. Di norma, i contenitori possono creare istanze delle dipendenze in modo ricorsivo.

Nota

I contenitori IoC spesso si basano sulla reflection in fase di runtime. Tuttavia, alcuni contenitori IoC usano meccanismi diversi per identificare le dipendenze da inserire nell'oggetto creato. Ad esempio, alcuni contenitori IoC usano attributi .NET e altri usano convenzioni di denominazione.

Funzionamento dei contenitori IoC

L'aggiunta di un contenitore IoC al codice può generare confusione nella gestione delle dipendenze. Si esaminerà ora il funzionamento di un contenitore dal punto di vista concettuale.

Diagramma che mostra il comportamento di un contenitore IoC, che inserisce una dipendenza SqliteRepo, che implementa un'interfaccia IDbRepository come livello di accesso ai dati.

Il diagramma precedente illustra il modo in cui il contenitore IoC elaborerà la classe DataAccessLayer.

public class DataAccessLayer
{
    public DataAccessLayer(
        IDbRepository db,
        IMessageDialog message)
    {
        ...
    }

    public ILogger Logger {get; set;}
    ...
}

Si supponga che il codice client richieda un DataAccessLayer. Passa quindi al contenitore per crearne uno.

  1. Il contenitore esamina il tipo DataAccessLayer e identifica il costruttore appropriato. Determina tutte le dipendenze di cui è a conoscenza.

  2. In questo caso, il livello di accesso ai dati richiede il passaggio di un oggetto IDbRepository al costruttore. Quando il contenitore rileva questa dipendenza, cerca nel proprio elenco di oggetti noti.

  3. Il contenitore fornisce un'implementazione esistente o crea un nuovo oggetto che possa soddisfare la dipendenza richiesta. Si noti la dipendenza, SqliteRepository. Tenere presente che è possibile commutare il repository in un provider XML o un provider di servizi Web, registrando una dipendenza diversa con il contenitore. La possibilità di commutare le dipendenze è un vantaggio importante del disaccoppiamento dell'applicazione in astrazioni. Il contenitore IoC può anche creare dipendenze registrate come classi reali e non solo astrazioni.

  4. Il contenitore richiama il costruttore per creare il livello di accesso ai dati, ottenendo un'istanza. Quindi, analizza tutte le proprietà dell'oggetto creato e cerca le eventuali dipendenze aggiuntive note. Imposta queste dipendenze su valori registrati specifici. Questo comportamento spesso è specifico del contenitore. Non si vuole impostare ogni singola proprietà, ma solo quelle specifiche. La maggior parte dei contenitori usa attributi o indicatori per indicare le proprietà da fornire.

Come usare un contenitore IoC

Verrà ora esaminato un esempio che usa il contenitore per l'inserimento delle dipendenze.

Ricordare che si è richiesto che la classe DataAccessLayer accetti un oggetto IDbRepository e un oggetto IMessageDialog come parametri del costruttore. Sarà necessario verificare che queste astrazioni siano registrate con il contenitore. La registrazione delle astrazioni in genere fa parte dell'inizializzazione dell'applicazione. Questo processo assicura che i tipi siano disponibili per l'uso.

Si supponga di voler registrare le astrazioni per iOS. In genere si userà la classe delegata UIApplication, come per il processo di registrazione del localizzatore di servizi. L'esempio presuppone che il nome del contenitore sia MyContainer.

public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    MyContainer container = new MyContainer();

    public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
    {
        ...
        container.Register<IDbRepository,SqliteRepository>();
        container.Register<IMessageDialog,MessageDialog_iOS>();
        container.Register<ILogger>(new AzureLogger(AzureToken));
        ...
    }
}

Il primo passaggio consiste nel creare o ottenere l'accesso al contenitore IoC. Successivamente, si registrerà ogni astrazione nota con il contenitore IoC.

Si noti che il contenitore IoC consente diversi tipi di registrazioni. È possibile associare interfacce a tipi specifici in cui il contenitore IoC crea l'oggetto. In alternativa, si può creare l'istanza autonomamente e usare un costruttore specifico, ad esempio AzureLogger.

Nel codice, nel punto in cui è necessaria un'istanza di DataAccessLayer, si userà il contenitore IoC per creare l'oggetto.

var dataLayer = container.Create<DataAccessLayer>();

Ricordare che questo passaggio viene eseguito nel codice specifico della piattaforma. Il contenitore IoC crea istanze di tipi specifici della piattaforma e include un elenco di astrazioni registrate. Creerà l'oggetto DataAccessLayer e lo passerà automaticamente al repository e alla finestra di dialogo.

Spesso si noterà che i tipi le cui istanze sono create da un contenitore IoC, ad esempio la classe DataAccessLayer, sono configurati come singleton. Dopo la creazione dell'istanza dell'oggetto statico, è possibile usarlo in qualsiasi parte dell'applicazione.

Avviso

Prestare attenzione alla durata degli oggetti. Gli oggetti statici restano attivi per la durata dell'applicazione. Se contengono oggetti grafici di grandi dimensioni, nell'applicazione possono verificarsi problemi di allocazione della memoria. Valutare l'opportunità di creare istanze Just-In-Time degli oggetti e deallocarli quando non sono più necessari.

Ecco la stessa configurazione per un'applicazione Android.

public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    MyContainer container = new MyContainer();

    protected override void OnCreate(Bundle savedInstanceState)
    {
        ...
        container.Register<IDbRepository,SqliteRepository>();
        container.Register<IMessageDialog,MessageDialog_Android>();
        container.Register<ILogger>(new AzureLogger(AzureToken));
        ...
    }
}

Vantaggi dei contenitori DI e IoC

I contenitori DI e IoC offrono i vantaggi chiave seguenti:

  • Il codice client ha bisogno solo di dipendenze reali. Non è necessario alcun riferimento a un contenitore.
  • Usando l'inserimento delle dipendenze, l'identificazione delle dipendenze usate risulta semplice. Spesso le dipendenze vengono passate ai costruttori o inserite in proprietà.

Svantaggi dei contenitori DI e IoC

I contenitori DI e IoC presentano gli svantaggi seguenti:

  • L'uso dello schema non è intuitivo. Il quadro generale può essere difficile da comprendere.
  • Lo schema spesso richiede una qualche forma di reflection. In genere la reflection non influisce sulle prestazioni, ma in alcuni casi potrebbe farlo. Ad esempio, alcuni contenitori possono analizzare gli assembly all'avvio dell'applicazione e registrare automaticamente le dipendenze. Non usare la funzionalità di analisi all'avvio dal contenitore in uso. Le analisi all'avvio influiscono negativamente sulle prestazioni dell'applicazione.