Speciale CoseNonJaviste

Dagger: dependency injection su Java e Android (I parte)di

In questo articolo affrontiamo l'argomento Dagger, un framework che sfrutta la dependency injection e che presenta alcune caratteristiche che ne stanno decretando il successo, soprattutto in ambito Android: è abbastanza semplice, può essere usato in diversi ambienti e fa solo poche cose ma le fa bene.

Introduzione

La dependency injection è ormai un pattern consolidato nel mondo dello sviluppo software: il primo documento che ne formalizza i concetti principali è di Martin Fowler [1] e risale al gennaio 2004. Il primo framework che sfrutta la dependency injection, e quello tutt'ora più diffuso, è Spring, la cui versione 1.0 è del 24 marzo 2004 [2].

In questo articolo vedremo le caratteristiche principali di Dagger, ponendo un'attenzione particolare sulla scrittura dei test. Dagger è un framework di dependency injection sviluppato da Square che sta diventando quasi uno standard nel mondo Android ma che può essere usato anche in altri ambienti. A differenza di Spring, che mette a disposizione tantissime funzionalità, Dagger è molto semplice e permette "solamente" di gestire gli oggetti sfruttando la dependency injection. La semplicità è una scelta architetturale fortemente voluta dagli autori: i concetti da capire sono pochi ma il framework è comunque molto potente.

Caratteristiche principali di Dagger

Il nome Dagger deriva dall'acronimo DAG: Directed Acyclic Graph. Infatti gli oggetti gestiti da Dagger sono collegati fra loro formando un grafo aciclico; uno dei punti forza di Dagger è la validazione di questo grafo in modo da rilevare eventuali errori in fase di compilazione del codice.

La tagline sul sito di Dagger definisce questo framework: "A fast dependency injector for Android and Java". Ma perchè tutta questa enfasi sull'essere veloce? Cosa ha di diverso Dagger dagli altri framework di Dependency Injection?

Annotation processing

La differenza principale è che, invece di sfruttare la reflection Java per analizzare le classi e costruire gli oggetti, Dagger si basa sull'annotation processing [3].In pratica, subito dopo la normale compilazione del codice Java, viene eseguito l'annotation processor di Dagger che analizza le classi annotate, valida la configurazione e genera delle classi che poi saranno sfruttate runtime. I vantaggi di questo approccio sono molteplici:

  • per non avere un ritardo nel primo avvio, l'analisi delle classi viene effettuata a tempo di compilazione e non a runtime usando la reflection;
  • se ci sono degli errori di configurazione, questi vengono segnalati in fase di compilazione;
  • le classi generate sono usate a runtime per evitare di utilizzare la reflection e non avere quindi problemi di prestazioni neanche in ambienti con risorse limitate.

La configurazione di come costruire gli oggetti è fatta esclusivamente con annotation e codice Java; a differenza di altri framework non c'è da scrivere una sola riga di XML!

Una classe di esempio

Per capire meglio come utilizzare Dagger, vediamo un esempio pratico di utilizzo. Il codice che vedremo è codice Java "puro", in modo da focalizzarsi sull'utilizzo di Dagger: non ci sono riferimenti ne' al runtime di Android ne' a framework web. La classe principale dell'esempio permette di eseguire il download e stampare su console una lista di repository da github:

public class RepositoryListUiBean {
    public void printRepositories() {
        try {
            Gson gson = new Gson();
            Repository[] repositories = gson.fromJson(download("https://api.github.com/repositories"),
                Repository[].class);
            for (Repository repository : repositories) {
                System.out.println(repository.getName() 
                     + " - " + repository.getDescription());
            }
         } catch (IOException e) {
                System.out.println(e.getMessage());
         }
    }
    //...
}

La classe Repository è un classico bean Java:

public class Repository {
    private long id;
    private String name;
    private String description;
 
    public long getId() {
        return id;
    }
 
    public String getName() {
        return name;
    }
 
    public String getDescription() {
        return description;
    }
 
    @Override
    public String toString() {
        return name;
    }
}

È stato omesso il metodo download che esegue una chiamata all'URL passato come parametro e restituisce la stringa contenente il risultato della chiamata al server.

Il metodo printRepositories, pur essendo lungo poche righe, contiene il codice che implementa almeno tre funzionalità: download dei dati, parsing di un JSON e stampa di oggetti. Il parsing e la stampa su console sono molto semplici in questo esempio ma in casi reali potrebbero essere più complessi: per esempio potrebbero coinvolgere il mapping in JSON di più classi e creare un report in PDF o XLS invece che scrivere su console.

Possiamo invocare il metodo printRepositories da linea di comando aggiungendo un metodo main:

public static void main(String[] args) {
    RepositoryListUiBean repositoryListUiBean = new RepositoryListUiBean();
    repositoryListUiBean.printRepositories();
}

Tutta la logica di questa semplice applicazione è in pratica inclusa in un solo metodo. Questa scelta non è ovviamente la migliore se vogliamo testare il comportamento usando JUnit. A dire il vero, una classe di test la possiamo scrivere facilmente:

public class RepositoryListUiBeanTest {
 
    @Test
    public void testPrintRepositories() throws Exception {
        RepositoryListUiBean repositoryListUiBean 
            = new RepositoryListUiBean();
        repositoryListUiBean.printRepositories();
    }
}

I limiti del test

Ma questo test ci può essere di aiuto in qualche modo? Non proprio, poiche' ci sono vari problemi in questo test:

  • essendo una sola classe, non possiamo testare le varie funzionalità (download, parsing e stampa) in modo autonomo;
  • il test esegue una chiamata HTTP verso un server esterno, quindi nel caso di problemi di connessione avremo una eccezione;
  • non possiamo verificare in modo semplice l'output del test visto che il metodo non restituisce niente e stampa solo sullo standard output.

Andiamo avanti con l'esempio per capire come la dependency injection e Dagger in particolare possono aiutarci a risolvere questi problemi.

Introduciamo la dependency injection

Prima di introdurre Dagger facciamo un piccolo refactoring; dividiamo il download e il parsing dalla stampa della lista introducendo la classe RepositoryService:

public class RepositoryService {
 
    public Repository[] retrieveRepositories() throws IOException {
        Gson gson = new Gson();
        return gson.fromJson(download("https://api.github.com/repositories"), 
                 Repository[].class);
    }
 
    //...
}

Scriviamo anche un test per verificare il comportamento di questa classe:

@Test
public void testRetrieveRepositories() throws Exception {
    RepositoryService repositoryService = new RepositoryService();
    Repository[] repositories = repositoryService.retrieveRepositories();
    Assert.assertNotNull(repositories);
}

Anche in questo caso continuiamo a fare la chiamata al server, ma almeno abbiamo il download e il parsing testabili indipendentemente dalla stampa.

Con l'introduzione di questa nuova classe, dentro RepositoryListUiBean rimane solo la logica per la stampa:

public class RepositoryListUiBean {
    private RepositoryService repositoryService;
 
    public RepositoryListUiBean(RepositoryService repositoryService) {
        this.repositoryService = repositoryService;
    }
 
    public void printRepositories() {
        try {
            Repository[] repositories = repositoryService.retrieveRepositories();
            for (Repository repository : repositories) {
                System.out.println(repository.getName() + " - " 
                    + repository.getDescription());
            }
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}

Abbiamo passato volutamente l'oggetto RepositoryService nel costruttore invece che istanziarlo direttamente in questa classe. Già questo può essere considerato un esempio di dependency injection: le dipendenze delle classe provengono dall'esterno e non sono create internamente alla classe. Può sembrare una modifica banale, ma ci permette di testare in isolamento la classe RepositoryListUiBean, vediamo come.

Per prima cosa, scriviamo una nuova classe che estende RepositoryService e che invece di eseguire una chiamata HTTP restituisce una lista di tre elementi costruiti al volo:

public class RepositoryServiceStub extends RepositoryService {
    @Override
    public Repository[] retrieveRepositories() throws IOException {
        return new Repository[] {
            new Repository(1, "repo1", "repo1"),
            new Repository(2, "repo2", "repo2"),
            new Repository(3, "repo3", "repo3")
        };
    }
}

Le classi come questa vengono in genere chiamate stub e servono soltanto all'interno di un test e permettono di simulare i metodi di una classe restituendo dei dati preparati appositamente. Gli stub hanno un ruolo fondamentale nei test in quanto permettono di "rompere" le dipendenze fra le classi: in questo caso ci permettono di testare la classe RepositoryListUiBean indipendemente dal RepositoryService.

A questo punto il test è facile da scrivere:

@Test
public void testPrintRepositories() throws Exception {
    RepositoryListUiBean repositoryListUiBean 
             = new RepositoryListUiBean(new RepositoryServiceStub());
    repositoryListUiBean.printRepositories();
}

Abbiamo fatto un passo avanti nei test; adesso possiamo eseguire la stampa senza eseguire la chiamata HTTP.

Configurazione di un progetto Dagger

È arrivato il momento di introdurre Dagger nel nostro progetto di esempio. Abbiamo già detto che Dagger utilizza l'annotation processing; per questo non basta aggiungere il JAR nel classpath ma dobbiamo anche configurare l'IDE. Questa configurazione non è complicata da impostare: potete consultare la documentazione dell'IDE che utilizzate per capire come eseguirla.

Se utilizzate Maven o Gradle, la configurazione è ancora più semplice; per Maven basterà aggiungere queste righe di XML al file pom.xml:

    com.squareup.dagger
    dagger
    1.2.1


    com.squareup.dagger
    dagger-compiler
    1.2.1
    true

Se usate Gradle, che sta diventando lo standard nei progetti Android, il codice da aggiungere è il seguente:

compile 'com.squareup.dagger:dagger-compiler:1.2.1'
compile 'com.squareup.dagger:dagger:1.2.1'

Moduli e Object Graph

Iniziamo a introdurre Dagger per gestire queste due classi tramite dependency injection. Come detto, per usare Dagger non è necessario scrivere neanche una riga di XML: possiamo configurare tutto usando annotations o codice Java. Uno dei concetti più importanti di Dagger è quello di modulo: un modulo è una classe Java annotata con @Module che si occupa di istanziare i vari oggetti. Ogni oggetto viene creato da un metodo annotato con l'annotation @Provide. Nel nostro caso il modulo si occuperà di istanziare le nostre due classi:

@Module(injects = RepositoryListUiBean.class)
public class CnjModule {
    @Singleton @Provides
    public RepositoryService provideRepositoryService() {
        return new RepositoryService();
    }
 
    @Provides
    public RepositoryListUiBean provideRepositoryListUiBean(RepositoryService repositoryService) {
        return new RepositoryListUiBean(repositoryService);
    }
}

Alcuni aspetti importanti

Un paio di cose da notare in questa classe:

  • all'annotation @Module possiamo passare vari parametri di configurazione di un modulo; in questo caso, usiamo il parametro inject per definire qual è la classe che verrà esposta verso l'esterno;
  • attraverso l'annotation @Singleton stiamo definendo il RepositoryService come singleton; Dagger creerà solo un'istanza di questa classe e restituirà sempre questa istanza ogni volta che ce ne sarà bisogno;
  • il metodo provideRepositoryListUiBean ha un parametro di tipo RepositoryService; quando sarà invocato, Dagger passerà in automatico il risultato dell'invocazione del metodo provideRepositoryService; in questo caso, come in tutti gli altri contesti di dependency injection in Dagger, il matching su quale oggetto passare avviene in base al tipo;
  • per quanto detto, ci può essere una sola definizione per ogni classe, altrimenti Dagger segnalerà un errore durante l'analisi delle dipendenze; possiamo definire più oggetti dello stesso tipo usando l'annotation @Named o una annotation custom annotata a sua volta con l'annotation @Qualifier.

L'altro concetto importante di Dagger è quello di ObjectGraph: partendo da uno o più moduli possiamo creare un ObjectGraph che racchiude e gestisce a runtime tutti gli oggetti gestiti da Dagger. In pratica un ObjectGraph è il cuore di una applicazione che utilizza Dagger, istanzia gli oggetti e gestisce le dipendenze.

ObjectGraph

Possiamo quindi creare un ObjectGraph partendo dal modulo appena visto, e ottenere un oggetto RepositoryListUiBean richiamando il metodo get:

ObjectGraph objectGraph = ObjectGraph.create(new CnjModule());
RepositoryListUiBean repositoryListUiBean = objectGraph.get(RepositoryListUiBean.class);

In alcuni casi, utilizzando un altro framework non abbiamo il controllo sulla creazione degli oggetti: per esempio le Activity di una applicazione, ma anche i backing bean JSF e gli EJB sono creati in automatico. In questi casi possiamo comunque utilizzare Dagger, ma dobbiamo utilizzare un metodo diverso rispetto a quello appena visto. In pratica non dobbiamo creare un nuovo oggetto ma dobbiamo eseguire l'injection delle dipendenze in un oggetto esistente. In questo caso possiamo utilizzare il metodo inject di un object graph che esegue proprio questa operazione.

Annotation

Come promesso, non abbiamo scritto file XML di configurazione (a parte, ovviamente, quello molto limitato di Maven) ma comunque abbiamo scritto le dichiarazioni degli oggetti in una classe Java. C'è un altro modo per configurare gli oggetti gestiti da Dagger, perche' è possibile utilizzare l'annotation @Inject. Possiamo aggiungere questa annotation al costruttore di RepositoryService:

@Singleton
public class RepositoryService {
    @Inject public RepositoryService() {
    }
    //...
}

Anche nel caso della classe RepositoryListUiBean possiamo aggiungere @Inject al costruttore:

public class RepositoryListUiBean {
    private RepositoryService repositoryService;
 
    @Inject public RepositoryListUiBean(RepositoryService repositoryService) {
        this.repositoryService = repositoryService;
    }
    //...
}

Oppure, più semplicemente, possiamo annotare direttamente il campo (per poter funzionare con l'annotation processing il campo non deve essere privato):

public class RepositoryListUiBean {
    @Inject RepositoryService repositoryService;
    //...
}

A questo punto possiamo semplificare la definizione del modulo togliendo le due definizioni degli oggetti:

@Module(injects = RepositoryListUiBean.class)
public class CnjModule {
}

Definizioni esplicite

L'uso dell'annotation @Inject è molto comodo, ma in alcuni casi è necessario definire gli oggetti in modo esplicito dentro un modulo:

  • quando lavoriamo con le interfacce Java dobbiamo creare un metodo annotato con @Provide che avrà nella signature l'interfaccia e che ritornerà l'implementazione;
  • se abbiamo delle classi che non possiamo modificare per aggiungere delle annotation, possiamo utilizzarle con Dagger ma siamo obbligati a definirle in un modulo;
  • se vogliamo configurare un oggetto calcolando dei parametri, possiamo aggiungere della logica dentro il metodo del modulo.

Visualizzazione dell'object graph

A questo punto abbiamo un grafo di oggetti gestito da Dagger: nel nostro caso il grafo è molto semplice in quanto è composto da soli due nodi, ma in progetti veri il grafo può essere anche molto complesso. Se andiamo a curiosare nella directory in cui ci sono i file .class vedremo che sono presenti anche dei file con estensione .dot. Aprendo questi file con Graphviz [4], vediamo la rappresentazione grafica del nostro grafo di oggetto (figura 1)

 

 

Figura 1 - La rappresentazione grafica del nostro grafo: in questo caso è molto semplice (solo due nodi) ma anche rappresentazioni molto più complesse possono essere visualizzate semplicemente aprendo i file .dot presenti nella directory delle classi.

 

Magari non sarà il massimo da un punto di vista della presentazione, ma ottenere un grafico di questo tipo non ha alcun costo in termini di tempo e impegno, poiche' basta, appunto, aprire i file .dot.

Conclusioni

In questo articolo abbiamo visto le caratterestiche principali di Dagger;  in particolare abbiamo visto cos'è un modulo e come creare un object graph partendo da un modulo. Ora che conosciamo i concetti base di Dagger, restano però da risolvere tutti i problemi del primo test che abbiamo scritto. Per questo ci sarà la seconda parte di questa miniserie, in cui vedremo alcune funzionalità avanzate di Dagger con particolare attenzione al testing del codice.

 

Riferimenti

[1] Martin Fowler, "Inversion of Control Containers and the Dependency Injection pattern"

http://www.martinfowler.com/articles/injection.html

 

[2] Spring Framework 1.0 Final Released

https://spring.io/blog/2004/03/24/spring-framework-1-0-final-released

 

[3] JSR 269: Pluggable Annotation Processing API

https://www.jcp.org/en/jsr/detail?id=269

 

[4] Graphviz: Graph Visualization Software

http://www.graphviz.org/

 

 

 

 

Condividi

Pubblicato nel numero
195 maggio 2014
Fabio Collini è un Software Architect che si occupa di progettazione e sviluppo di applicazioni sulle piattaforme Java EE e Android. Da agosto 2009 è uno sviluppatore Android frellance e, in questa sua attività, ha rilasciato due applicazioni nell’Android Market: Apps Organizer e Folder Organizer. Presso Nana Bianca si occupa…
Articoli nella stessa serie
Ti potrebbe interessare anche