Inserimento delle dipendenze
Nota
Questo eBook è stato pubblicato nella primavera del 2017 e non è stato aggiornato da allora. C'è molto nel libro che rimane prezioso, ma alcuni dei materiali sono obsoleti.
In genere, un costruttore di classe viene richiamato quando si crea un'istanza di un oggetto e tutti i valori necessari all'oggetto vengono passati come argomenti al costruttore. Questo è un esempio di inserimento delle dipendenze e, in particolare, è noto come inserimento del costruttore. Le dipendenze necessarie per l'oggetto vengono inserite nel costruttore.
Specificando le dipendenze come tipi di interfaccia, l'inserimento delle dipendenze consente di separare i tipi concreti dal codice che dipende da questi tipi. In genere usa un contenitore che contiene un elenco di registrazioni e mapping tra interfacce e tipi astratti e i tipi concreti che implementano o estendono questi tipi.
Esistono anche altri tipi di inserimento delle dipendenze, ad esempio l'inserimento di setter di proprietà e l'inserimento di chiamate al metodo, ma sono meno comunemente visti. Pertanto, questo capitolo si concentrerà esclusivamente sull'esecuzione dell'inserimento del costruttore con un contenitore di inserimento delle dipendenze.
Introduzione all'inserimento delle dipendenze
L'inserimento delle dipendenze è una versione specializzata del modello Inversion of Control (IoC), in cui il problema invertito è il processo di recupero della dipendenza richiesta. Con l'inserimento delle dipendenze, un'altra classe è responsabile dell'inserimento di dipendenze in un oggetto in fase di esecuzione. Nell'esempio di codice seguente viene illustrato come strutturare la classe ProfileViewModel
quando si usa l'inserimento delle dipendenze:
public class ProfileViewModel : ViewModelBase
{
private IOrderService _orderService;
public ProfileViewModel(IOrderService orderService)
{
_orderService = orderService;
}
...
}
Il ProfileViewModel
costruttore riceve un'istanza IOrderService
come argomento, inserita da un'altra classe. L'unica dipendenza nella ProfileViewModel
classe è sul tipo di interfaccia. Pertanto, la ProfileViewModel
classe non ha alcuna conoscenza della classe responsabile della creazione di un'istanza dell'oggetto IOrderService
. La classe responsabile della creazione di un'istanza dell'oggetto e dell'inserimento IOrderService
nella ProfileViewModel
classe è nota come contenitore di inserimento delle dipendenze.
I contenitori di inserimento delle dipendenze riducono l'accoppiamento tra oggetti fornendo una funzionalità per creare istanze di classe e gestirle in base alla configurazione del contenitore. Durante la creazione degli oggetti, il contenitore inserisce tutte le dipendenze richieste dall'oggetto. Se tali dipendenze non sono ancora state create, il contenitore crea e risolve prima le relative dipendenze.
Nota
L'inserimento delle dipendenze può anche essere implementato manualmente usando le factory. Tuttavia, l'uso di un contenitore offre funzionalità aggiuntive, ad esempio la gestione della durata e la registrazione tramite l'analisi degli assembly.
L'uso di un contenitore di inserimento delle dipendenze offre diversi vantaggi:
- Un contenitore rimuove la necessità di una classe per individuare le relative dipendenze e gestirle.
- Un contenitore consente il mapping delle dipendenze implementate senza influire sulla classe .
- Un contenitore facilita la testabilità consentendo la simulazione delle dipendenze.
- Un contenitore aumenta la manutenibilità consentendo di aggiungere facilmente nuove classi all'app.
Nel contesto di un'app Xamarin.Forms che usa MVVM, in genere verrà usato un contenitore di inserimento delle dipendenze per la registrazione e la risoluzione dei modelli di visualizzazione e per la registrazione dei servizi e l'inserimento nei modelli di visualizzazione.
Sono disponibili molti contenitori di inserimento delle dipendenze, con l'app per dispositivi mobili eShopOnContainers usando TinyIoC per gestire l'istanza delle classi del modello di visualizzazione e del servizio nell'app. TinyIoC è stato scelto dopo aver valutato una serie di contenitori diversi e offre prestazioni superiori sulle piattaforme per dispositivi mobili rispetto alla maggior parte dei contenitori noti. Facilita la creazione di app ad accoppiamento libero e fornisce tutte le funzionalità comunemente disponibili nei contenitori di inserimento delle dipendenze, inclusi i metodi per registrare i mapping dei tipi, risolvere gli oggetti, gestire la durata degli oggetti e inserire oggetti dipendenti nei costruttori di oggetti risolti. Per altre informazioni su TinyIoC, vedere TinyIoC in github.com.
In TinyIoC il TinyIoCContainer
tipo fornisce il contenitore di inserimento delle dipendenze. La figura 3-1 mostra le dipendenze quando si usa questo contenitore, che crea un'istanza di un IOrderService
oggetto e la inserisce nella ProfileViewModel
classe .
Figura 3-1: Dipendenze quando si usa l'inserimento delle dipendenze
In fase di esecuzione, il contenitore deve conoscere l'implementazione dell'interfaccia di cui deve creare un'istanza IOrderService
prima di poter creare un'istanza di un ProfileViewModel
oggetto. Questo implica:
- Contenitore che decide come creare un'istanza di un oggetto che implementa l'interfaccia
IOrderService
. Questa operazione è nota come registrazione. - Contenitore che crea un'istanza dell'oggetto che implementa l'interfaccia
IOrderService
e l'oggettoProfileViewModel
. Questa operazione è nota come risoluzione.
Alla fine, l'app terminerà l'uso dell'oggetto ProfileViewModel
e diventerà disponibile per l'operazione di Garbage Collection. A questo punto, il Garbage Collector deve eliminare l'istanza IOrderService
se altre classi non condividono la stessa istanza.
Suggerimento
Scrivere codice indipendente dal contenitore. Provare sempre a scrivere codice indipendente dal contenitore per separare l'app dal contenitore di dipendenze specifico in uso.
Registrazione
Prima che le dipendenze possano essere inserite in un oggetto, i tipi delle dipendenze devono essere prima registrati con il contenitore. La registrazione di un tipo comporta in genere il passaggio del contenitore di un'interfaccia e un tipo concreto che implementa l'interfaccia.
Esistono due modi per registrare i tipi e gli oggetti nel contenitore tramite il codice:
- Registrare un tipo o un mapping con il contenitore. Se necessario, il contenitore compilerà un'istanza del tipo specificato.
- Registrare un oggetto esistente nel contenitore come singleton. Se necessario, il contenitore restituirà un riferimento all'oggetto esistente.
Suggerimento
I contenitori di inserimento delle dipendenze non sono sempre adatti. L'inserimento delle dipendenze introduce complessità e requisiti aggiuntivi che potrebbero non essere appropriati o utili per le piccole app. Se una classe non ha dipendenze o non è una dipendenza per altri tipi, potrebbe non essere opportuno inserirla nel contenitore. Inoltre, se una classe ha un singolo set di dipendenze che sono integrali al tipo e non cambierà mai, potrebbe non essere opportuno inserirla nel contenitore.
La registrazione dei tipi che richiedono l'inserimento delle dipendenze deve essere eseguita in un singolo metodo in un'app e questo metodo deve essere richiamato all'inizio del ciclo di vita dell'app per garantire che l'app sia consapevole delle dipendenze tra le relative classi. Nell'app per dispositivi mobili eShopOnContainers questa operazione viene eseguita dalla ViewModelLocator
classe , che compila l'oggetto TinyIoCContainer
ed è l'unica classe nell'app che contiene un riferimento a tale oggetto. L'esempio di codice seguente illustra come l'app per dispositivi mobili eShopOnContainers dichiara l'oggetto TinyIoCContainer
nella ViewModelLocator
classe :
private static TinyIoCContainer _container;
I tipi vengono registrati nel ViewModelLocator
costruttore. Questa operazione viene ottenuta creando prima un'istanza TinyIoCContainer
di , illustrata nell'esempio di codice seguente:
_container = new TinyIoCContainer();
I tipi vengono quindi registrati con l'oggetto e l'esempio TinyIoCContainer
di codice seguente illustra la forma più comune di registrazione del tipo:
_container.Register<IRequestProvider, RequestProvider>();
Il Register
metodo illustrato di seguito esegue il mapping di un tipo di interfaccia a un tipo concreto. Per impostazione predefinita, ogni registrazione dell'interfaccia viene configurata come singleton in modo che ogni oggetto dipendente riceva la stessa istanza condivisa. Pertanto, nel contenitore esisterà solo una singola RequestProvider
istanza, condivisa da oggetti che richiedono un'iniezione di un IRequestProvider
tramite un costruttore.
I tipi concreti possono anche essere registrati direttamente senza un mapping da un tipo di interfaccia, come illustrato nell'esempio di codice seguente:
_container.Register<ProfileViewModel>();
Per impostazione predefinita, ogni registrazione di classe concreta viene configurata come istanza multipla in modo che ogni oggetto dipendente riceva una nuova istanza. Pertanto, quando l'oggetto ProfileViewModel
viene risolto, verrà creata una nuova istanza e il contenitore inserirà le dipendenze necessarie.
Risoluzione
Dopo la registrazione di un tipo, può essere risolto o inserito come dipendenza. Quando un tipo viene risolto e il contenitore deve creare una nuova istanza, inserisce eventuali dipendenze nell'istanza di .
In genere, quando viene risolto un tipo, si verifica una delle tre operazioni seguenti:
- Se il tipo non è stato registrato, il contenitore genera un'eccezione.
- Se il tipo è stato registrato come singleton, il contenitore restituisce l'istanza singleton. Se è la prima volta che viene chiamato il tipo, il contenitore lo crea, se necessario, e mantiene un riferimento a esso.
- Se il tipo non è stato registrato come singleton, il contenitore restituisce una nuova istanza e non ne mantiene un riferimento.
L'esempio di codice seguente mostra come è possibile risolvere il RequestProvider
tipo registrato in precedenza con TinyIoC:
var requestProvider = _container.Resolve<IRequestProvider>();
In questo esempio viene chiesto a TinyIoC di risolvere il tipo concreto per il IRequestProvider
tipo, insieme a eventuali dipendenze. In genere, il Resolve
metodo viene chiamato quando è necessaria un'istanza di un tipo specifico. Per informazioni sul controllo della durata degli oggetti risolti, vedere Gestione della durata degli oggetti risolti.
L'esempio di codice seguente mostra come l'app per dispositivi mobili eShopOnContainers crea un'istanza dei tipi di modello di visualizzazione e le relative dipendenze:
var viewModel = _container.Resolve(viewModelType);
In questo esempio viene chiesto a TinyIoC di risolvere il tipo di modello di visualizzazione per un modello di visualizzazione richiesto e il contenitore risolverà anche eventuali dipendenze. Quando si risolve il ProfileViewModel
tipo, le dipendenze da risolvere sono un ISettingsService
oggetto e un IOrderService
oggetto . Poiché le registrazioni dell'interfaccia sono state usate durante la registrazione delle SettingsService
classi e OrderService
, TinyIoC restituisce le istanze singleton per le SettingsService
classi e OrderService
e quindi le passa al costruttore della ProfileViewModel
classe . Per altre informazioni su come l'app per dispositivi mobili eShopOnContainers costruisce i modelli di visualizzazione e li associa alle visualizzazioni, vedere Creazione automatica di un modello di visualizzazione con un localizzatore di modelli di visualizzazione.
Nota
La registrazione e la risoluzione dei tipi con un contenitore comportano un costo in termini di prestazioni perché il contenitore usa la reflection per la creazione di ogni tipo, soprattutto se le dipendenze vengono ricostruite per la navigazione di ogni pagina nell'app. Se le dipendenze presenti sono numerose o complete, il costo della creazione può aumentare in modo significativo.
Gestione della durata degli oggetti risolti
Dopo aver registrato un tipo usando una registrazione di classe concreta, il comportamento predefinito per TinyIoC consiste nel creare una nuova istanza del tipo registrato ogni volta che il tipo viene risolto o quando il meccanismo di dipendenza inserisce istanze in altre classi. In questo scenario, il contenitore non contiene un riferimento all'oggetto risolto. Tuttavia, quando si registra un tipo usando la registrazione dell'interfaccia, il comportamento predefinito per TinyIoC consiste nel gestire la durata dell'oggetto come singleton. Pertanto, l'istanza rimane nell'ambito mentre il contenitore è nell'ambito e viene eliminato quando il contenitore esce dall'ambito e viene sottoposto a Garbage Collection oppure quando il codice elimina in modo esplicito il contenitore.
È possibile eseguire l'override del comportamento di registrazione TinyIoC predefinito usando i metodi fluent AsSingleton
e AsMultiInstance
API. Ad esempio, il AsSingleton
metodo può essere usato con il Register
metodo , in modo che il contenitore crei o restituisca un'istanza singleton di un tipo quando si chiama il Resolve
metodo . Nell'esempio di codice seguente viene illustrato come Viene richiesto a TinyIoC di creare un'istanza singleton della LoginViewModel
classe :
_container.Register<LoginViewModel>().AsSingleton();
La prima volta che il LoginViewModel
tipo viene risolto, il contenitore crea un nuovo LoginViewModel
oggetto e ne mantiene un riferimento. In tutte le risoluzioni successive di LoginViewModel
, il contenitore restituisce un riferimento all'oggetto LoginViewModel
creato in precedenza.
Nota
I tipi registrati come singleton vengono eliminati quando il contenitore viene eliminato.
Riepilogo
L'inserimento delle dipendenze consente di separare i tipi concreti dal codice che dipende da questi tipi. In genere usa un contenitore che contiene un elenco di registrazioni e mapping tra interfacce e tipi astratti e i tipi concreti che implementano o estendono questi tipi.
TinyIoC è un contenitore leggero che offre prestazioni superiori sulle piattaforme mobili rispetto alla maggior parte dei contenitori noti. Facilita la creazione di app ad accoppiamento libero e fornisce tutte le funzionalità comunemente disponibili nei contenitori di inserimento delle dipendenze, inclusi i metodi per registrare mapping dei tipi, risolvere oggetti, gestire la durata degli oggetti e inserire oggetti dipendenti nei costruttori di oggetti risolti.