sabato 6 febbraio 2021

Dependency Injection

Cos'è e perché è importante

Come abbiamo visto nell'articolo sull'Inversion of  Control, la dependency injection è una tecnica che si prefigge di rendere indipendenti le classi dagli oggetti che esse utilizzano.

In sintesi, seguendo questo principio, un metodo di una classe non dovrebbe mai direttamente creare oggetti di altre classi. Chiamiamo client la classe utilizzatrice e service la classe utilizzata.

In questi termini, il motivo per cui la D.I. è importante è rendere possibile il cambio del service senza modificare il codice del client, ed è anche molto utile in fase di unit testing perché diventa facile passare degli stub al client ai fini di testarlo. Inoltre rende possibile la separazione delle funzioni, la leggibilità ed il riutilizzo.


Come si implementa

Si distinguono, nella D.I., quattro elementi principali:

  1. il servizio da usare (service)
  2. il client che utilizza il servizio
  3. l'interfaccia che il servizio espone e che viene usata nel client
  4. l'injector, la cui funzione è costruire il servizio e "iniettarlo" nel client
Ci possono essere principalmente tre modalità di injection, e si basano tutte su meccanismi di reflection ossia l'injector analizza il codice della classe da costruire e ne legge i costruttori o le proprietà pubbliche. Se le classi da iniettare richiedono a loro volta altre classi, questo può provocare una costruzione in cascata sempre con lo stesso meccanismo.

Constructor injection

Le dipendenze sono passate nel costruttore della classe dall'injector.
Esempio

public interface B{.. };
public class A {
   B service;
    A (B service) {
      this.service=service;
    }
}
l'injector in questo caso costruirà b di tipo B e chiamerà new A(b) per ottenere un oggetto di tipo A.

Questo tipo di costruzione ha lo svantaggio di richiedere immediatamente tutti i parametri e di non poterli più cambiare in seguito.

Setter injection

Il client espone delle proprietà pubbliche che l'injector valorizza quando crea l'oggetto.

Esempio

public interface B{.. };
public class A {
   B service{get; public set;}
}

l'injector in questo caso creerà una classe di tipo A e ne valorizzerà la proprietà:

var a = new A()
var b = (logica per costruire un oggetto di tipo B)
a.service = b;

Questo tipo di costruzione ha il vantaggio di poter decidere in momenti distinti i servizi da passare ad una classe, e però lascia aperto il problema di come accertarsi che tutte le dipendenze siano state passate prima di utilizzare i metodi del client

Interface injection

Il client espone un'interfaccia mediante la quale l'injector può  valorizzare il servizio.
Esempio
public interface B_Setter{
 public void setB(B service);
}

public class A : B_Setter {
 B service;
 public void setB(B service){
  this.service = service;
 }

}

L'injector in questo caso cercherà un metodo setter (con convenzioni stabilite nell'implementazione dell'injector), ed eseguirà qualcosa tipo:

A a = new A();
B b = (logica per costruire B);
A.setB(b);

Questo tipo di costruzione è utile se nell'assegnare il servizio si desidera anche fare altre operazioni sul servizio stesso, che mal sarebbero riposte nel semplice setter.


Quando utilizzare la dependency injection

Ci sono dei casi in cui è certamente consigliabile utilizzare meccanismi di d.i., e sono dipendenze le dipendenze non stabili, ossia soggette a possibili cambiamenti, come:
  • Oggetti che per essere creati hanno bisogno di una configurazione o che utilizzano risorse di sistema (ad esempio Database). Questi di solito sono anche quelli che creano i maggiori problemi in fase di unit test.
  • Istanze di classi che sono ancora in via di sviluppo o sono sviluppate da altri team, incluse classi presenti in librerie di terze parti
  • Istanze di classi che hanno un comportamento non deterministico, che crea in genere dei problemi nello unit test

Object Composition

Attraverso la Dependency Injection possiamo realizzare il paradigma dell'Object Composition, ossia costruire degli oggetti complessi a partire da  oggetti più semplici, che vengono combinati 
La Object composition può sfruttare la modalità di late-binding presente nella dependency injection ed essere maggiormente efficace, potendo decidere dinamicamente quali saranno le classi da utilizzare.

Gestione del ciclo di vita (Object lifetime)

Avendo il client rinunciato a costruire in autonomia gli oggetti che implementano i servizi, rimane aperto il problema di quando vanno distrutti tali oggetti. Questo deve essere gestito in qualche modo dal framework.

Altri usi

Visto che le classi concrete usate per fornire i servizi sono decise all'esterno del client, si può sfruttare il meccanismo di injection anche per modificare le classi, non solo ai fini del test,  ma per passare altre classi  che forniscano comportamenti aggiuntivi, come applicare il logging, meccanismi di sicurezza, inviare messaggi ad altre classi, o di profiling.
La dependency injection dunque è uno strumento utile anche a realizzare altri design pattern, come vedremo nei futuri articoli.

Nessun commento:

Posta un commento

Refactoring: improving modularization

  Modularità In generale, per modularità si intende la misura in cui un sistema può essere decomposto e ricombinato. La modularizzazione è a...