Dependency Injection con Google Guice

Da qualche tempo sono alla ricerca della giusta combinazione di strumenti per comporre uno stack “leggero” per i miei progetti meno impegnativi. Qualcosa di semplice ma efficace da utilizzare quando non è richiesta la potenza di fuoco di Spring.

Un componente al quale, però, non voglio rinunciare è un solido motore di dependency injection, che mi semplifichi la vita e mi permetta d’implementare architetture pulite e facilmente testabili.

Tra le diverse opzioni esistenti ho deciso di andare sul sicuro e di scegliere un progetto piuttosto rodato: Google Guice. In questo post diamo un’occhiata generale al framework ed esaminiamo qualche caso d’uso.

Per cominciare

Per utilizzare Guice nel nostro progetto ci basterà aggiungere una sola dipendenza:

<dependency>
  <groupId>com.google.inject</groupId>
  <artifactId>guice</artifactId>
  <version>4.2.0</version>
</dependency>

Inoltre esiste una serie di moduli aggiuntivi che estendono le funzionalità del framework e che possono essere importati all’occorrenza.

Nota: Dando un’occhiata alle release notes dell’ultima versione (ad oggi, la 4.2), vediamo che c’è anche la possibilità di escludere il modulo AOP, nel caso non fossimo interessati a questa funzionalità.

Dependency Injection in azione

I meccanismi di dependency injection di Guice ruotano attorno a due componenti principali: le interfacce Module e Injector.

Molto semplicemente, utilizzeremo i moduli per configurare le nostre dipendenze e a definire il binding tra interfacce e classi; utilizzeremo invece l’injector per ottenere istanze delle classi configurate nei nostri moduli.

Vediamo un esempio:

public class Exp1 extends AbstractModule {
    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new Exp1());
        StringPrinter stringPrinter = injector.getInstance(StringPrinter.class);
        stringPrinter.print("Hello!");
    }

    @Override
    protected void configure() {
        bind(StringPrinter.class).to(SimpleStringPrinter.class);
    }
}

Come vedete, il framework mette a disposizione la classe AbstractModule, che permette di definire velocemente le nostre configurazioni/associazioni andando a sovrascrivere il metodo configure.

Nel metodo main noterete invece che ho prima di tutto creato un Injector a partire dal modulo Exp1, dopodiché l’ho utilizzato per ottenere un’istanza della classe StringPrinter.

Facile, no?

Esaminiamo ora alcune delle modalità con cui Guice ci permette di definire il binding tra le nostre classi.

Linked binding e instance binding

Quanto visto nell’esempio precedente è il modo più semplice con cui possiamo definire un’associazione tra un’interfaccia e una classe concreta nel nostro progetto. Guice definisce questa modalità linked binding:

Tramite il metodo bind possiamo legare un’interfaccia ad una sua implementazione:

bind(StringPrinter.class).to(SimpleStringPrinter.class);

Prendiamo invece questo esempio di configurazione:

@Override
protected void configure() {
    bind(StringProcessor.class).toInstance(new MultiplierStringProcessor(3));
    bind(StringPrinter.class).to(AdvancedStringPrinter.class);
}

In questo caso non lasciamo che il framework produca automaticamente l’istanza per noi, ma stiamo esplicitamente definendo come deve essere costruita l’istanza di MultiplierStringProcessor associata all’interfaccia StringProcessor. Questo meccanismo si chiama instance binding.

Se andiamo invece a curiosare nella classe AdvancedStringPrinter, noteremo che il costruttore presenta l’annotazione @Inject, che ha un significato analogo all’annotazione @Autowired di Spring:

@Inject
public AdvancedStringPrinter(StringProcessor processor) {
    this.processor = processor;
}

La dipendenza di tipo StringProcessor verrà iniettata tramite il costruttore in base a quanto previsto nel modulo di configurazione.

Named dependencies e binding annotations

Partiamo da un altro esempio di configurazione:

@Override
protected void configure() {
    bind(StringProcessor.class)
            .annotatedWith(Names.named("reverse"))
            .to(ReverseStringProcessor.class);
    bind(StringPrinter.class).to(AnotherAdvancedStringPrinter.class);
}

Come vedete, in questo caso il binding include qualcosa in più: il metodo annotatedWith permette di introdurre una condizione nell’associazione definita. In questo esempio stiamo “spiegando” a Guice che le dipendenze denominate reverse dovranno essere risolte utilizzando un’istanza di tipo ReverseStringProcessor.

Non a caso il costruttore di AnotherAdvancedStringPrinter presenta l’annotazione @Named:

@Inject
public AnotherAdvancedStringPrinter(@Named("reverse") StringProcessor processor) {
    this.processor = processor;
}

Il metodo annotatedWith inoltre permette l’utilizzo di annotazioni custom che possono essere utili per identificare le dipendenze all’interno del nostro progetto.

Metodi Provides e scopes

All’interno dei nostri moduli potremmo avere l’esigenza d’istanziare oggetti complessi che difficilmente trovano spazio in un toInstance (come visto nel paragrafo relativo all’instance binding).

In tal caso possiamo utilizzare l’annotazione @Provides come in questo esempio:

@Override
protected void configure() {
    bind(StringProcessor.class)
            .annotatedWith(Names.named("reverse"))
            .to(ReverseStringProcessor.class)
            .in(Scopes.SINGLETON);
    bind(StringPrinter.class).to(BetterAdvancedStringPrinter.class);
}

@Provides
@Named("defaultHello")
String defaultString() {
    return "Default Hello!";
}

Piccola nota Anche in questo caso possiamo fare un’analogia con Spring e in particolare con l’annotazione @Bean.

Nel nostro esempio il metodo defaultString crea una stringa denominata defaultHello; la classe BetterAdvancedStringPrinter utilizzerà questa stringa nel costruttore:

@Inject
public BetterAdvancedStringPrinter(
        @Named("reverse") StringProcessor processor,
        @Named("defaultHello") String defaultMessage) {
    this.processor = processor;
    this.defaultMessage = defaultMessage;
}

Altro concetto importante è quello di scope: di default Guice restituisce una nuova istanza ogni volta che l’injector viene invocato; è però possibile configurare un comportamento diverso in fase di binding.

Nell’esempio all’inizio del paragrafo vediamo come sia possibile indicare, tramite il metodo in, un differente “tempo di vita” per gli oggetti istanziati. Ad esempio, nel nostro caso, un’unica istanza (singleton) di tipo ReverseStringProcessor andrà a soddisfare tutte le dipendenze di tipo StringProcessor.

In conclusione

Google Guice è sicuramente un’interessante opzione se stiamo cercando una soluzione… lightweight. Un DI framework flessibile e senza troppi fronzoli che però svolge egregiamente il suo compito.

Inoltre, Guice supporta anche la programmazione orientata agli aspetti: funzionalità da non sottovalutare, che però non ho ancora avuto modo di testare.

A dire la verità, anche il fratello minore, Dagger, sembra un progetto molto interessante; chissà… magari a breve avrò modo di farci un giro.

A presto,

David