Finalist

Finalist Developers Blog

Dependency Injection met Guice

21 May 2007 11:14 · Rob Schellhorn · Java

Object georiënteerd programmeren kenmerkt zich door een groot probleem op te breken in steeds kleinere units, van applicatie naar module naar object. Door het combineren van de objecten, ontstaan afhankelijkheden, of referenties. Deze afhankelijkheden, ook wel koppeling genoemd, moeten geminimaliseerd worden, want ze maken het vrijwel onmogelijk om te wisselen van implementatie. Dat kan problematisch zijn wanneer objecten getest moet worden.


Zie het volgende voorbeeld:

class ConsoleLogger {
	public void log(String text) {
		System.out.println(text);
	}
}
 
class Foo {
	private ConsoleLogger logger = new ConsoleLogger();
 
	public void bar() {
		logger.print( "Hello, world!" );
	}
}

De klasse Foo is nu afhankelijk van een specifieke Logger implementatie. Stel nu dat er niet direct naar System.out geprint moet worden, maar naar een grafische interface. Een logische stap is om de log functionaliteit te abstraheren in een Logger interface en twee implementaties te maken:

interface Logger {
    void log( String text );
}
 
class ConsoleLogger implements Logger {
	public void log(String text) {
		System.out.println(text);
	}
}
 
class UILogger implements Logger {
	private Text textComponent = /* Some component */ ;
 
	public void log(String text) {
		textComponent.append(text);
	}
}

Maar hoe komt de klasse Foo nu aan een implementatie van Logger? Natuurlijk is het mogelijk om een concrete Logger implementatie te initialiseren:

class Foo{
	private Logger printer = new ConsoleLogger();
 
	// ...
}

Hierdoor verbetert de situatie nauwelijks. Het probleem zit hem in de plek van de instantiatie van de Logger, wat binnen Foo gebeurt. De afhankelijkheid naar de concrete Logger implementatie ligt vast binnen de Foo klasse (al dan niet direct via een Factory of Service Locator). Dependency Injection maakt het mogelijk om deze afhankelijkheden buiten de implementatie om te configureren.

Guice

Guice is een dependency injection framework, gemaakt door een tweetal medewerkers van Google. Een groot verschil met andere frameworks is de minimale configuratie die nodig is om te werken. Ook gebeurt de configuratie niet in tientallen xml bestanden, maar gewoon in Java. Dankzij dependency injection, hoeft Foo zich niet druk hoeft te maken waar de logger vandaan komt, die wordt geïnjecteerd:

class Foo{
	@Inject private Logger logger;
 
	public void bar() {
		logger.print( "Hello, world!" );
	}
}

Guice kan niet alleen injecteren via velden, maar ook via methoden of via een constructor. Elk alternatief heeft zijn voordelen en nadelen. In het kort geven velden de kortste syntax, maar maken testen onmogelijk. Injectie staat het toe om bepaalde acties te ondernemen, wanneer een afhankelijkheid geïnjecteerd wordt. En injectie via een constructor staan het toe om geïnjecteerde velden final te maken. De keuze zal per geval verschillen.
Het bovenstaande voorbeeld is echter niet compleet. Guice moet geconfigureerd worden, welke Logger implementatie geïnjecteerd moet worden. Dit gebeurd via modules.

class LoggerModule implements Module {
	public void configure( Binder binder ) {
		binder.bind( Logger.class ).to( ConsoleLogger.class );
	}
}

In een module kunnen een willekeurige hoeveelheid bindingen gemaakt worden. Ze stellen je dus in staat, bepaalde afhankelijkheden te koppelen en andere weer configureerbaar te maken.

Vervolgens moeten de modules geladen worden in Guice. Het resultaat is een injector, een object dat andere objecten creëert en zorgt voor de afhankelijkheden.

public void startup() {
	Injector injector = Guice.createInjector( new LoggerModule() );
	Foo foo = injector.getInstance( Foo.class ); // Instantieert Foo en injecteert afhankelijkheden
	foo.bar(); // De geïnjecteerde logger zal gebruikt worden.
}

Wat de Garbage Collector is voor het opruimen van objecten, zo kan je de Injector beschouwen voor het maken van objecten. Naast deze basis functionaliteit, biedt Guice nog een aantal leuke features.

Scopes

Guice zal voor elk plek waar een object geïnjecteerd moet worden een nieuwe instantie maken van het gevraagde type. Soms echter, wordt binnen een bepaalde scope een zelfde instantie van een bepaald type verwacht. Neem bijvoorbeeld de sessie scope in een web applicatie. Een sessie behoort toe aan een gebruiker. Elke verwijzing naar gebruiker binnen de sessie moet naar de zelfde instantie verwijzen. In Guice is dit mogelijk op de volgende manier:

class AuthorisationModule implements Module {
	public void configure( Binder binder ) {
		binder.bind( User.class ).to( SessionUser.class ).in(ServletScopes.SESSION);
	}
}

Naast de sessie scope bestaat ook de request en signleton scope. Daarnaast is het mogelijk om zelf scopes toe te voegen.

Providers

Soms is het niet mogelijk om in de module al te weten, welke concrete implementatie gekozen moet worden. Bijvoorbeeld, elke dag van de week moet er alleen naar de console gelogd worden, maar in het weekend naar een bestand. In zo’n geval kan een provider uitkomst bieden:

class LoggerProvider implements Provider {
	private ConsoleLogger consoleLogger;
	private FileLogger fileLogger;
 
	public Logger get() {
		return isWeekend() ? fileLogger : consoleLogger;
	}
}
 
class LoggerModule implements Module {
	public void configure( Binder binder ) {
		binder.bind( Logger.class ).toProvider( LoggerProvider.class );
	}
}

Context specifieke injectie

Dependency injection is ook erg handig om constanten te binden. Neem het volgende voorbeeld:

class Range {
	@Inject private int minimum;
	@Inject private int maximum;
 
	// ...
}

Op deze manier kan geen onderscheid gemaakt worden tussen minimum en maximum, omdat beide integers zijn. De meeste frameworks kunnen onderscheid maken door te ’switchen’ op de naam. Dit is foutgevoelig, want als de naam verandert, moet deze ook in de configuratie aangepast worden. Guice heeft een andere aanpak, in plaats van strings worden marker annotations gebruikt:

class Range {
	@Inject @TheMinimum private int minimum;
	@Inject @TheMaximum private int maximum;
 
	// ...
}
 
class RangeModule implements Module {
	public void configure( Binder binder ) {
		binder.bindConstant(int.class).annotatedWith(TheMinimum.class).to(1);
		binder.bindConstant(int.class).annotatedWith(TheMaximum.class).to(12);
	}
}

Conclusie

Dependency injection stelt de programmeur in staat echt te programmeren tegen interfaces en de implementatie te verbergen. Dit maakt het switchen van implementatie mogelijk. Grote, dure objecten kunnen vervangen worden door simpele mock objecten om snel te unit testen.

Guice is een dependecy injection framework dat zich onderscheid door de minimale benodigde hoeveelheid configuratie. Alles wordt in Java code geconfigureerd. Dankzij de Java 5 taal features annotations en generics is het geheel ook typesafe. Een presentatie waarin nog veel meer over Guice verteld wordt, is hier te bekijken.

—————————————————————————————
Meer weten over Java-specialist Finalist IT Group?

Share and Enjoy:
  • E-mail this story to a friend!
  • Print this article!
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Blogosphere News
  • Fleck
  • NuJIJ
  • Slashdot
  • StumbleUpon
  • LinkedIn
  • Twitter

Reageer

RSS feed for comments on this post · TrackBack URI