Wstrzykiwanie zależności czyli Dependency Injection w 9 minut i 59 sekund. Część 4: wykorzystanie Spring-a
04.30.2009Jak widzieliśmy [część 1., część 2., część 3.] wstrzykiwanie zależności jest całkiem sympatycznym pomysłem (o ile się go poprawnie używa), ale można zapytać się, na ile jest to kosztowne? Przez koszty rozumiem tutaj nakład pracy, jaki trzeba włożyć w używanie architektury wykorzystującej DI.
Wyobraźmy sobie, że nasza aplikacja często wykorzystuje klasę NewsService. Co to oznacza w praktyce? Za każdym razem musimy wstrzykiwać do niej zależność, których potrzebuje. Nasz przykład jest dość prosty, zależności nie ma zbyt wiele, ale nawet tutaj powtarzanie przy każdym użyciu kodu
public class Client {
public static void main(String[] args) {
Authenticator authenticator = new UsernamePassAuthenticator("beer","beer");
Driver driver = new SqlDbDriver();
//inject dependency #1
Storage storage = new DBStorage(driver);
//inject dependency #2
NewsService newsService = new NewsService(storage);
//inject dependency #3
newsService.setAuthenticator(authenticator);
newsService.addNews("ble ble ble");
}
}
nie jest zbyt zachęcające. Czy nie lepiej jest użyć jednak jakiego wzorca factory czy service locator, żeby jednak NewsService sam sobie znalazł potrzebne klasy?
Okazuje się, że problem można rozwiązać bez rezygnowania z DI wykorzystując kontenera Dependency Injection, taki jak Spring, PicoContainer czy Google Guice.
Cóż takiego nadzwyczajnego robią te kontenery, że ludzie ich używają. Ich działanie tak na prawdę sprowadza się do tego, że pozwalają w jakiś sposób w jednym miejscu powiedzieć, że dana klasa potrzebuje konkretnej implementacji interfejsu lub interfejsów.
W naszym przykładzie chcemy wskazać, że NewsService potrzebuje klasy DBStorage, która implementuje Storage, a klasa DBStorage potrzebuje klasy SqlDbDriver, implementującej interfejs Driver.
Kontenery różnią się od siebie sposobem definiowania zależności między klasami. Spring pozwala konfigurować te zależności w pliku XML-owym lub (od niedawna) przy pomocy metadanych (ang. annotations). Google Guice jest nowszym rozwiązaniem i bazuje na wykorzystaniu metadanych.
Można, rzecz jasna, napisać własnych “kontener DI”, czyli pojedynczą klasę, w której będzie zawarta informacją o zależnościach między klasami, ale to oznacza dodatkową pracę, zastanawianie się na ile nasz pomysł na kontener będzie uniwersalny, itp.
Zaletą centralnej konfiguracji zależności jest możliwość ich podmieniania w razie potrzeby. Na przykład na potrzeby testów możemy jako implementacji Storage używać jakiejś naszej klasy, która udaje Storage na tyle, na ile jest to potrzebne do testów jednostkowych czy funkcjonalnych. Dzięki temu testy wykonują się szybciej, nie jest potrzebna baza danych.
Dodatkowo, jeżeli klient zażyczy sobie, żeby dane były przechowywane w plikach tekstowych, a nie bazie danych, to wystarczy, że napiszemy odpowiednie implementację Storage i/lub Driver i zmienimy konfigurację wstrzykiwania zależności w ten sposób, żeby były użyte nowe implementacje.
Oczywiście nie ma nic za darmo, to my musimy utrzymywać plik konfiguracyjny, który potrafi być całkiem pokaźnych rozmiarów w większej aplikacji lub pilnować aktualizacji parametrów odpowiednich metadanych.
Swoją drogą jest jedna technologia, w której mamy do dyspozycji DI, a nie musimy sami nic konfigurować – to te paskudne EJB, przez wielu szczerze znienawidzone. Tam po prostu serwer aplikacji przechowuje informację o komponentach, które mają być udostępniane w ramach aplikacji i jeżeli chcemy jakiegoś komponentu użyć, to po prostu odwołujemy się do niego poprzez interfejs, a serwer sam nam wstrzykuje odpowiedni obiekt:
@Stateless
public class PierwszyBean implements PierwszyBeanInterfejs {
//implementację DrugiBean wstrzyknie sam serwer aplikacji
@EJB DrugiBeanInterfejs drugiBean;
public void metoda(){
drugiBean.zrobCos();
}
}
Nie mamy żadnego pliku konfiguracyjnego, w którym byśmy mogli godzinami szukać źle zdefiniowanych zależności ;).
Ok, tak na serio pilnowanie zależności nie jest aż tak bardzo straszne. Użycie kontenerów zazwyczaj jednak ułatwia życie, zobaczmy więc jak działa najpopularniejszy z nich: Spring.
Pierwsza rzecz, to musimy sobie ściągnąć ze strony http://www.springsource.org/ Spring Framework. Ja używałem wersji 2.5.X.
Prościej sprawę można załatwić używając rozsądnego IDE, które ma wbudowane wsparcie dla Spring-a. W paczce dostajemy to razem z NetBeans, MyEclipse IDE, IntelliJ Idea. Jak ktoś używa Eclipse, to można sobie doinstalować odpowiednią wtyczkę. Użycie IDE ma głęboki sens, bo dostajemy podpowiadanie składni w XML-owym pliku konfiguracyjnym, a czasem nawet wizualne narzędzie, które pozawala łatwiej oglądać zależności.
Zakładamy zatem, że mamy na ścieżce klas dodane biblioteki Spring-a.
Co teraz? Czy musimy jakoś modyfikować klasy które już napisaliśmy? Na szczęście nie, nasza aplikacja używa już DI, więc nawet linijki kodu ruszyć nie musimy. Zrobić musimy dwie rzeczy. Po pierwsze musimy zadeklarować zależności między klasami. Po drugie musimy zrobić użytek z tej konfiguracji modyfikując odpowiednio klasę Client, która używa NewsService.
Zacznijmy od konfiguracji. Plik może mieć dowolną nazwę, tutaj jest to springXMLConfig.xml, a jego zawartość widzimy na poniższym listingu:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean name="sqldriver" class="pl.erudis.newsservice.di.drivers.SqlDbDriver"/>
<bean name="dbstorage" class="pl.erudis.newsservice.di.storage.DBStorage">
<constructor-arg ref="sqldriver"/>
</bean>
<bean name="upauth" class="pl.erudis.newsservice.di.auth.UsernamePassAuthenticator" />
<bean name="newsService" class="pl.erudis.newsservice.di.NewsService" >
<property name="authenticator" ref="upauth"/>
<property name="storage" ref="dbstorage"/>
</bean>
</beans>
Co się tutaj dzieje. Na początku deklarujemy użycie klasy SqlDbDriver. Podobnie deklarujemy użycie klasy DBStorage, dodatkowo zaznaczamy, że klasa DBStorage będzie używała klasy SqlDbDriver. Odpowiada za to element
Mówi on Springowi, żeby przy inicjalizacji klasy DBStorage do konstruktora “wstrzyknął” obiekt wskazanej przez nas klasy, czyli SqlDbDriver.
Dalej wszystko robimy analogicznie, deklarujemy użycie klasy UsernamePassAuthenticator, a następnie informujemy Spring, że NewsService potrzebuje klas DBStorage oraz UsernamePassAuthenticator. W tym przypadku obiekty klas nie są przekazywane jako parametry konstruktora, tylko inicjalizują pola klasy NewsService, używając odpowiednich metod “set” dla pól.
Wreszcie nadszedł czas na nagrodę, czyli wykorzystanie Springa do zarządzania zależnościami, zobaczmy jak wygląda teraz klasa kliencka:
package pl.erudis.newsservice.di;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
*
* @author Piotr Kochański, www.xoft.pl
*/
public class SpringClient {
public static void main(String[] args) {
ApplicationContext factory = new ClassPathXmlApplicationContext("META-INF/springXMLConfig.xml");
NewsService newsService = (NewsService)factory.getBean("newsService");
newsService.getAuthenticator().login(new String[]{"beer", "beer"});
newsService.addNews("bla bla spring bla");
}
}
Pierwszym krokiem jest inicjalizacja Springa, jest to robione przy użyciu klasy ApplicationContext – musimy po prostu powiedzieć Springowi, żeby wczytał z pliku konfiguracyjnego zestaw zależności. Zgodnie z często używaną konwencją plik konfiguracyjny Springa jest w katalogu META-INF, który muszą “widzieć” skompilowane klasy Java.
Po inicjalizacji zostaje już nie wiele do zrobienia, wyciągamy klasę, która nas interesuje, czyli NewsService przy pomocy ApplicationContext przekazując mu nazwę tej klasy skonfigurowaną w springXMLConfig.xml. Spring automatycznie przekaże do NewsService potrzebne zależności i dalej używamy jej zapominając o DI czy Springu.
Zaleta wykorzystania kontenera DI jest jasna – mamy spójny sposób deklarowania zależności między komponentami, dzięki czemu pisząc kod możemy się koncentrować na jego faktycznej funkcjonalności, a nie pilnować odpowiednich związków z innymi klasami.
Oczywiście Spring nie jest tylko samym kontenerem DI, potrafi znacznie więcej rzeczy, ale tutaj nas interesował tylko ten jego aspekt.
Źródła przykładowej aplikacji można ściągnąć klikając tutaj, jeżeli ktoś woli, to jest także do pobrania projekt NetBeans-a w wersji 6.5, który zawiera tę aplikację.
W kolejnej części cyklu zobaczymy w jaki sposób użyć kontenera Google Guice do zarządzania zależnościami w DI.
04.30.2009 at 7:27 po południu
Fajnie się czyta. Bardzo klarownie wyjaśnione DI i w jaki sposób skorzystać ze springowego DI. Nawet, jeśli wiedziałem przed przeczytaniem, o czym będzie, nie sądzę, abym stracił czas. Czekam na Guice.
09.24.2009 at 2:04 po południu
qolupixety…
cassidy freestyles lyrics …