Wstrzykiwanie zależności czyli Dependency Injection. Część 5: wykorzystanie Google Guice
07.09.2009W poprzednim odcinku widzieliśmy, jak można sobie ułatwić życie wstrzykując zależności do klas przy pomocy Springa [reszta cyklu: część 1., część 2., część 3.]. Dzisiaj czas na coś prostszego i przyjemniejszego: Google Guice!
Przypomnijmy sobie jaki problem rozwiązujemy. Otóż mamy klasę NewsService, która potrzebuje do działania dwóch innych klas, które są konkretnymi implementacjami interfejsów Authenticator oraz Storage. Dodatkowo Storage potrzebuje implementacji klasy Driver.
Dzięki temu, że NewsService zależy od interfejsów, a nie klas konkretnych możemy wstrzykiwać mu taką implementację danego interfejsu, jaka jest potrzebna w określonej sytuacji. Jak stwierdziliśmy, ręczne wstrzykiwanie na dłuższą metę jest męczące, w związku z tym postanowiliśmy użyć do tego celu Spring-a. Działa on w ten sposób, że w pliku konfiguracyjnym określamy zależności między klasami, które potem Spring już samodzielnie wstrzykuje we wskazana miejsce.
Podejście użyte przez Spring-a ma parę wad. Po pierwsze jest XML-owy plik konfiguracyjny, który musi być zsynchronizowany z kodem. Można w nim zrobić pomyłkę (dobre IDE pomaga unikać błędów), refaktoryzacja kodu jest utrudniona – dobre IDE pozwala w miarę niezawodnie przeprowadzać tę operację, ale czasem i ono może pomylić się, w końcu XML to tylko łańcuchy znaków. Wreszcie do pliku Springa czasem trzeba dodać więcej informacji, niż to jest tak na prawdę potrzebne (wersja 2.0 ograniczyła ten problem).
Czy można zrobić kontener DI lepiej? Narzuca się oczywiście użycie metadanych (ang. annotations), w końcu od Java 5.0 jest to nowa, skuteczna metoda kontroli zachowania aplikacji w określonym środowisku.
Warto się zastanowić dwa razy nad użyciem tego mechanizmu, bo zewnętrzna konfiguracja wcale nie jest taką złą rzeczą. Na przykład kwerendy nazwane dla JPA zdecydowanie wygodniej jest definiować w pliku, mimo, że można użyć metadanych.
Zobaczymy jednak, że Google Guice oferuje nam rozwiązanie, bazujące na wykorzystaniu metadanych, które faktycznie jest wygodniejsze w użyciu niż Spring.
Jak działa Guice? W kodzie umieszczamy metadane, które mówią gdzie potrzebujemy wstrzyknięcia zależności. Zajmuje się tym metadana @Inject.
Samo powiedzenie, że nasza klasa potrzebuje wstrzyknięcia instancji innej klasy to jeszcze za mało. Musimy powiedzieć o jaką konkretną implementację nam chodzi. W przypadku Spring-a ta informacja była zaszyta w konfiguracji w pliku XML. Google Guice podchodzi do tego tematu inaczej. Definicje zależności między klasami umieszczamy w osobnej klasie Java – module Guice. Dzięki temu definicje te są odporne na literówki i poddają się bezboleśnie refaktoryzacji.
Zobaczmy jak to wygląda w praktyce. Pierwsza rzecz, to trzeba pobrać Google Guice i dołączyć zawarte w ściągniętej paczce pliki JAR do ścieżki swojego projektu.
Gdy mamy już skonfigurowany do pracy projekt jesteśmy gotowi zacząć pracę z Guice. Przede wszystkim musimy zmodyfikować klasę NewsService:
import com.google.inject.Inject;
import pl.erudis.newsservice.di.storage.Storage;
import pl.erudis.newsservice.di.auth.Authenticator;
public class NewsService {
Storage storage;
Authenticator authenticator;
public NewsService() {
}
public NewsService(Storage storage) {
this.storage = storage;
}
public Authenticator getAuthenticator() {
return authenticator;
}
@Inject
public void setAuthenticator(Authenticator authenticator) {
this.authenticator = authenticator;
}
public Storage getStorage() {
return storage;
}
@Inject
public void setStorage(Storage storage) {
this.storage = storage;
}
public void addNews(String news){
if(authenticator != null){
authenticator.authenticate();
}
storage.save(news.getBytes());
System.out.println("News saved...");
}
}
Klasa ta w zasadzie nie zmieniła się w stosunku do wersji używanej ze Springiem, czy w ogóle bez kontenera DI, jedyna rzecz, która się pojawiła, to metadana @Inject w tych miejscach, gdzie potrzebne jest nam wstrzyknięcie instancji odpowiedniej klasy.
Jest to bardzo ważna rzecz: użycie Guice tak jak Springa nie wymaga zmian w kodzie – metadane to nie jest tak na prawdę zmiana kodu, tylko szczególny sposób konfiguracji aplikacji na potrzeby jakiegoś narzędzia.
Podobnie musimy zmodyfikować klasę DBStorage:
import com.google.inject.Inject;
import pl.erudis.newsservice.di.drivers.Driver;
public class DBStorage implements Storage{
Driver driver;
@Inject
public DBStorage(Driver driver) {
this.driver = driver;
}
public void save(byte[] data) {
driver.openConnection();
System.out.println("Saving in database: '" + new String(data) +"'");
driver.closeConnection();
}
}
Teraz pozostaje tylko powiedzieć co ma być wstrzykiwane w oznaczone metadaną @Inject miejsca. W tym celu definiujemy moduł:
import com.google.inject.Binder;
import com.google.inject.Module;
import pl.erudis.newsservice.di.auth.Authenticator;
import pl.erudis.newsservice.di.auth.UsernamePassAuthenticator;
import pl.erudis.newsservice.di.drivers.Driver;
import pl.erudis.newsservice.di.drivers.SqlDbDriver;
import pl.erudis.newsservice.di.storage.DBStorage;
import pl.erudis.newsservice.di.storage.Storage;
public class NewsServiceModule implements Module{
public void configure(Binder binder) {
binder.bind(Authenticator.class).to(UsernamePassAuthenticator.class);
binder.bind(Driver.class).to(SqlDbDriver.class);
binder.bind(Storage.class).to(DBStorage.class);
}
}
Moduł wiąże ze sobą interfejsy, których używamy w kodzie z konkretnymi implementacjami, wybranymi przez nas w danej sytuacji. Warto zwrócić uwagę na bardzo inteligentnie wybrane nazwy metod. Kod czyta się jak narrację, w końcu język programowania ma być językiem. Trudno się nie połapać co oznacza kod “powiąż Authenticator.class z UsernamePassAuthenticator.class”.
Widać też od razu, że bardzo łatwo dynamicznie tworzyć powiązania – wszystko robimy w kodzie Java.
Wreszcie ostatni element, czyli klasa kliencka
import com.google.inject.Guice;
import com.google.inject.Injector;
public class GuiceClient {
public static void main(String[] args) {
Injector i = Guice.createInjector(new NewsServiceModule());
NewsService newsService = i.getInstance(NewsService.class);
newsService.getAuthenticator().login(new String[]{"beer", "beer"});
newsService.addNews("bla bla guice bla");
}
}
Zarówno klasa kliencka jak i definicja modułów mają bardzo istotną zaletę w stosunku do Spring-a – nigdzie nie występują łańcuchy znaków reprezentujące klasy. Używały wyłącznie kompilowalnych artefaktów, dzięki czemu automatycznie odpada nam cała klasa przykrych błędów.
Jeżeli ktoś interesuje się działaniem Spring Framework, to pewnie wie, że od pewnego czasu także Spring może być konfigurowany przy pomocy metadanych, są jednak bardzo zasadnicze różnice pomiędzy sposobem robienia tego przez Spring-a i przez Guice, o tym będziemy mówić w kolejnym odcinku naszej serii.
07.14.2009 at 9:14 po południu
dzięki za bardzo ciekawy cykl o DI :) naprawdę fajnie wytłumaczone :)
pozdrawiam,
Paweł