Wstrzykiwanie zależności, czyli Dependency Injection w 9 minut i 59 sekund. Część 1: podręczny przewodnik tworzenia złych aplikacji
12.23.2008Gdzie się człowiek nie obejrzy tam się czai Dependency Injection (DI), czyli wstrzykiwanie zależności. Żeby jeszcze było tego mało, jak się zaczyna grzebać w internecie, to się co chwila można potknąć o jakiś framework, kontener czy coś takiego, co nam samo może zrobić Dependency Injection.
Co to w ogóle jest, po co takie coś komukolwiek, jak tego używać? Okazuje się, że sprawa jest prosta, wręcz banalna, a jednocześnie prowadzi do całkiem interesujących zastosowań. Za chwilę postaramy się zrozumieć jak DI działa, zrobimy to w sposób praktyczny, obdarty z krążących wokół DI ideologii i zupełnie zbędnego adżajlowego bełkotu.
Przekonamy się, że DI jest jednym z wielu możliwych sposobów budowania architektury aplikacji tak, by była łatwa w rozbudowie i testowaniu.
Będziemy chcieli zobaczyć coś więcej, niż zupełnie trywialny przykład (takich jest mnóstwo w internecie), po którym w zasadzie można wzruszyć ramionami, bo nie specjalnie widać tam jakiekolwiek zalety architektury wykorzystującej DI.
Naszym celem będzie zbudowanie bardzo prostej aplikacji służącej do przechowywania informacji, będzie składała się ona z trzech warstw (trójka jest nieprzypadkowa, sporo aplikacji Java EE jest rozbijane na tyle warstw) i tyluż komponentów.
Naszą aplikację napiszemy na kilka sposobów:
- naiwne podeście z paskudną architekturą;
- w miarę rozsądnie wyglądająca aplikacja, wykorzystująca wstrzykiwanie zależności;
- aplikacja wykorzystująca wstrzykiwanie zależności, ale zrobiona źle – warto wiedzieć, że wbrew wrażeniu, jakie można odnieść po ohah i ahah na temat DI, przy jego pomocy też można koncertowo skopać architekturę tworzonego oprogramowania.
- ta sama aplikacja, wykorzystująca wstrzykiwanie zależności i kontener Spring;
- jeszcze raz to samo, ale z wykorzystaniem Google Guice;
- następnie spróbujemy porównać działanie Spring i Google Guice;
- wreszcie, na końcu trochę “filozofii”, czy faktycznie DI jest potrzebne, czy jest potrzebne tylko Javie, jak to jest z innymi językami.
Zatem, do dzieła!
Zaczniemy od pierwszej wersji naszej aplikacji. Głównym jej elementem jest klasa NewsService, która używa do przechowywania informacji klasy DBStorage, która to z kolei potrzebuje sterownika komunikującego się z bazą danych (DBDriver). Chcemy mieć także możliwość zmuszenia użytkownika naszej aplikacji do uwierzytelnienia się – zajmuje się tym DBStorage i NewsService.
Zobaczmy sobie po kolei te klasy:
public class NewsService {
DBStorage dBStorage = new DBStorage();
public void addNews(String news){
addNews(news, new DBStorage());
}
public void addNews(String news, String uname, String pass){
addNews(news, new DBStorage(uname, pass));
}
public void addNews(String news, DBStorage dBStorage){
this.dBStorage = dBStorage;
dBStorage.save(news.getBytes());
}
}
W tej klasie ważne są dwie rzeczy: użycie DBStorage oraz oczywiście metoda addNews.
public class DBStorage {
DBDriver driver;
public DBStorage(String uname, String pass) {
authenticate(uname, pass);
driver = DBDriver.getInstance();
}
public DBStorage() {
driver = DBDriver.getInstance();
}
public void authenticate(String uname, String pass){
System.out.println("Authentication...");
}
public void save(byte[] data){
driver.openConnection();
System.out.println("Saving in database...");
driver.closeConnection();
}
}
Klasa DBStorage dzielnie sama sobie produkuje DBDriver i jeszcze na dodatek zajmuje się uwierzytelnianiem użytkowników. Metoda save wykonuje czarną robotę zapisywania danych w odpowiednim miejscu.
public class DBDriver {
private DBDriver() {
}
public static DBDriver getInstance(){
return new DBDriver();
}
public void openConnection(){
System.out.println("Openning the connection");
}
public void closeConnection(){
System.out.println("Closing the connection");
}
}
Zobaczmy też na listingu poniżej jak tego wszystkiego można użyć razem:
public class Client {
public static void main(String[] args) {
NewsService newsService = new NewsService();
newsService.addNews("ble ble ble");
DBStorage dbStorage1 = new DBStorage();
newsService.addNews("bla bla bla", dbStorage1);
DBStorage dbStorage2 = new DBStorage("joe","beer");
newsService.addNews("bla bla bla", dbStorage2);
}
}
No dobrze, kod się kompiluje, działa, wydawałoby się, że wszystko jest ok, ale… No właśnie, coś tutaj jednak nie gra.
Pierwsza rzecz. Wyobraźmy sobie, że chcemy teraz zapisywać dane nie w bazie danych tylko, powiedzmy, w pliku XML-owym, albo pliku JSON czy CSV. Albo może nasze newsy mają być przechowywane jako zserializowane obiekty Java. Jednakże klasa NewsService zależy od klasy DBStorage, która potrafi dane przechowywać tylko w bazie danych.
Mało tego, klasa DBStorage jest powiązana ściśle z DBDriver, także podmiana klasy sterownika również nie jest prosta.
Jedyne wyście jakie nam pozostaje, gdy chcemy zmienić sposób działania aplikacji, to dodawać do klasy NewsService kolejne metody dodające informację, które będą wykorzystywały inne sposoby przechowywania danych. Powstanie w ten sposób kod, który będzie się w dużej mierze powtarzał, klasa NewsService będzie się coraz bardziej rozrastała, a jej użycie coraz trudniejsze.
Świetnie, krytykować zawsze jest łatwo. A co, jeśli docelowym sposobem przechowywania informacji jest baza danych i nie będziemy tutaj nic zmieniać?
To nas prowadzi do drugiego problemu – testowalności tej aplikacji. Jedynym sposobem testowania klasy NewsService jest dostarczenie jej instancji klasy DBStorage – bez niej NewsService nie może w ogóle działać. DBStorage z kolei używa konkretnego sterownika do bazy danych.
W rezultacie, jeśli chcemy napisać test jednostkowy, czy test funkcjonalny musimy odtworzyć pełne środowisko działania aplikacji. Jeżeli nawet da się to zrobić (chociaż może wcale tak nie być i często w rzeczywistych sytuacjach nie jest), to testy będą znacznie trudniejsze do napisania, będą dłużej się wykonywały. Przy takiej aplikacji jak nasza, pewnie to nie jest problemem, ale pracując z 500 tysiącami linii kodu może być to istotna sprawa.
Na koniec jeszcze jeden rzecz – problem uwierzytelniania. Abstrahując od tego, że przywiązujemy się tylko do jednego sposobu uwierzytelniania (hasło + nazwa użytkownika), to jeszcze sam proces uwierzytelniania jest rozwleczony pomiędzy klasę NewsService i DBStorage.
Co jest przyczyną wymienionych problemów? Popatrzmy sobie na to, co zrobiliśmy z lotu ptaka:
Teraz widać jaśniej, co zrobiliśmy źle:
- Klasa
NewsServicezależy od konkretnej implementacjiDBStorage, przez co nie możemy go łatwo wymienić na inny typ kontenera na dane. DBStoragezależy także bezpośrednio odDBDriver– tutaj sytuacja jest odrobinę lepsza, boDBDriverjest tworzony przy pomocy metody factory, co daję pewną elastyczność tworzenia tego obiektu. Niestety z punktu widzenia testowalności niewiele to nam daje – chcielibyśmy w miejsce tego sterownika podstawić obiekt symulujący zachowanie sterownika, na co obecna implementacja nie pozwala- W przypadku uwierzytelniania źle zaplanowaliśmy odpowiedzialność klas i zaszyliśmy na stałe konkretny sposób uwierzytelniania
Jak przemodelować tę aplikację, żeby pozbyć wyeliminować powyżej opisane jej wady? Tym zajmiemy się w kolejnym wpisie.
05.07.2009 at 11:44 przed południem
Dzieki wielkie juz pare atrukulow probowalem czytac na ten temat (slaby angielski) i za chole.. nie moglem zrozumiec po co DI i myslalme ze to jakas skomplikowana sprawa a to banalo jest ale przydatny banal :D Dla mnie jasno rzeczowo i na temat dzieki
03.06.2010 at 4:09 po południu
artykuł godny polecenia