Wstrzykiwanie zależności czyli Dependency Injection w 9 minut i 59 sekund. Część 2: o tym, co tak właściwie robi DI
12.28.2008W poprzednim wpisie znęcaliśmy się nad niezbyt gramotnie napisanym kawałkiem oprogramowania. Teraz przyszedł czas na napisanie wszystkiego tak, jak trzeba, w czym nam pomagać będzie właśnie wstrzykiwanie zależności.
Co nas najbardziej uwierało w poprzedniej wersji aplikacji? Tak na prawdę były to dwie rzeczy:
- Powiązania między klasami były zrealizowane przy użyciu referencji do konkretnych klas: jeżeli
NewsServicepotrzebował mechanizmu do przechowywania informacji, to dawaliśmy mu referencję do klasy, która potrafiła przechowywać dane w SQL-owej bazie danych. Zmiana sposobu przechowywania danych na inny wymagała zmian w wielu miejscach w kodzie. - Jeżeli klasa potrzebowała do pracy innej klasy, to sama musiała sobie utworzyć odpowiedni obiekt:
NewsServicepotrzebując klasy do przechowywania danych sam sobie tworzył jej instancję.
Co w takim razie robi wstrzykiwanie zależności? Mówiąc ogólnie, w DI chodzi o to, żeby nie wiązać się z inną klasą poprzez użycie jej implementacji, tylko poprzez interfejs, pod który można podpiąć dowolną klasę go implementującą.
Dodatkowo, jeżeli nasza klasa potrzebuje konkretnej implementacji tego interfejsu, to nie powinna tej implementacji sama szukać czy jej tworzyć. Odpowiedni obiekt musi zostać do klasy “wstrzyknięty” w momencie inicjalizacji.
Zobaczmy, jak to wygląda w praktyce.
Zacznijmy od klasy NewsService, której bezpośrednio używają klasy “interfejsu użytkownika”:
public class NewsService {
Storage storage;
Authenticator authenticator;
public NewsService() {
}
public NewsService(Storage storage) {
this.storage = storage;
}
public Authenticator getAuthenticator() {
return authenticator;
}
public void setAuthenticator(Authenticator authenticator) {
this.authenticator = authenticator;
}
public Storage getStorage() {
return storage;
}
public void setStorage(Storage storage) {
this.storage = storage;
}
public void login(String uname, String pass){
((UsernamePassAuthenticator)authenticator).setUname(uname);
((UsernamePassAuthenticator)authenticator).setPass(pass);
}
public void addNews(String news){
if(authenticator != null){
authenticator.authenticate();
}
storage.save(news.getBytes());
System.out.println("News saved...");
}
}
Pierwsza rzecz, którą zrobiliśmy, to usunęliśmy referencje do konkretnej implementacji klasy odpowiedzialnej za przechowywanie danych (DBStorage) i zamiast niej umieściliśmy referencję do interfejsu Storage:
public interface Storage {
void save(byte[] data);
}
Dzięki temu będziemy mogli w klasie NewsService używać dowolnej implementacji Storage. Inicjalizując klasę NewsService będziemy jej “wstrzykiwać” odpowiednią klasę implementującą interfejs Storage. Z praktycznego punktu widzenia na tym właśnie polega cała magia Dependency Injection!
Szczególnie warto zwrócić uwagę na kluczowy element DI. W klasie NewsService nie ma kodu, który by wyszukiwał implementację interfejsu Storage – gdybyśmy coś takiego umieścili, to zamiast DI używalibyśmy wzorca projektowego service locator, który jest obecnie nieco passe. NewsService oczekuje, że w czasie inicjalizacji ktoś mu poda odpowiednią implementację Storage. W naszym przypadku robi to klasa Client, której listing jest na samym dole wpisu.
Komunikację między klasami poprzez interfejsy demonstruje w sposób wyraźny poniższy diagram klas dla aplikacji:
Widać, że tak, jak chcieliśmy, NewsService zależy od innych klas wyłącznie poprzez interfejsy, nie przez implementacje.
Analogicznie DBStorage zależy od sterownika także poprzez interfejs Driver. W poprzedniej wersji aplikacji DBStorage był powiązany z konkretną wersją sterownika – DBDriver. Zobaczmy, jak wygląda kod, który implementuje zawartość diagramu.
public class DBStorage implements Storage{
Driver driver;
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();
}
}
Sam interfejs Driver wygląda następująco:
public interface Driver {
void openConnection();
void closeConnection();
}
Konkretną implementacją sterownika jest u nas klasa SqlDbDriver:
public class SqlDbDriver implements Driver{
public void openConnection() {
System.out.println("Openning the database connection");
}
public void closeConnection() {
System.out.println("Closing the database connection");
}
}
Pozostała jeszcze z poprzedniej wersji aplikacji byle jak zaimplementowana polityka uwierzytelniania. Teraz zrobimy to lepiej. W klasie NewsService, zgodnie z duchem DI, umieściliśmy odwołanie do interfejsu Authenticator:
public interface Authenticator {
void authenticate() throws AuthException;
}
AuthException jest wyjątkiem czasu wykonywania:
public class AuthException extends RuntimeException{
public AuthException(Throwable cause) {
super(cause);
}
public AuthException(String message, Throwable cause) {
super(message, cause);
}
public AuthException(String message) {
super(message);
}
public AuthException() {
}
}
W naszym kodzie używamy konkretnej implementacji uwierzytelniania:
public class UsernamePassAuthenticator implements Authenticator{
private String uname;
private String pass;
public UsernamePassAuthenticator() {
}
public String getPass() {
return pass;
}
public void setPass(String pass) {
this.pass = pass;
}
public String getUname() {
return uname;
}
public void setUname(String uname) {
this.uname = uname;
}
public UsernamePassAuthenticator(String uname, String pass) {
this.uname = uname;
this.pass = pass;
}
/**
* Username & password authenticator: username must be the same as
* password - do not use it in production system ;)
*/
public void authenticate() {
if(!uname.equals(pass)){
throw new AuthException("Authorization failed");
}
}
}
No i wreszcie nagroda za nasze wysiłki, czyli klasa kliencka:
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");
}
}
Widzimy, że przed użyciem NewsService musimy go skonfigurować, czyli “wstrzyknąć” mu obiekty klas, które są potrzebne mu do działania.
Na koniec ćwiczenie dla szanownego czytelnika. W końcu czasem warto się sprawdzić. Czy teraz już nasza aplikacja na prawdę wygląda tak, jak trzeba? Na pewno? Czy może gdzieś jeszcze coś nie gra tak jak trzeba? Może gdzieś oszukałem? Jakiś mały problemik, architektoniczna niedoskonałość?
???
Jeżeli ktoś poczuł ducha DI, to pewnie od razu zorientował się, gdzie jeszcze tkwi problem. W klasie NewsService nadal tkwi ukryta zależność od klasy konkretnej zamiast interfejsu – w metodzie login klasy NewsService jest robione rzutowanie na klasę konkretną UsernamePassAuthenticator.
Zależność ta jest o tyle nieprzyjemna, że można ją na pierwszy rzut oka przeoczyć, w szczególności nie widać jej na diagramie UML-owym.
Warto się pozbyć się tego niezbyt eleganckiego kodu, świadczy on tylko o tym, że źle zaplanowaliśmy odpowiedzialność klasy NewsService. Niby dlaczego nagle ma ona zajmować się logowaniem, skoro może to zrobić Authenticator. Metoda login jest zwyczajnie zbędna.
To prowadzi nas z kolei do następnego pytania, na ile łatwo jest błędnie zaprojektować architekturę aplikacji, gdy zdecydujemy się używać wstrzykiwania zależności? Zajmiemy się tym problemem w trzeciej części cyklu.
Wreszcie dociekliwy czytelnik może zacząć się zastanawiać, jak będzie wyglądało to wstrzykiwanie zależności, jeżeli będziemy często używać NewsService – w końcu za każdym razem będziemy musieli mu wstrzykiwać te potrzebne klasy, co oznacza pisanie w kółko tego samego kodu. Czy nie lepiej jest użyć jednak jakiego wzorca factory czy service locator, żeby jednak NewsService sam sobie znalazł potrzebne klasy. Z tym problemem można sobie także poradzić najłatwiej przy użyciu kontenera Dependency Injection, takiego jak Spring, PicoContainer czy Guice. Temu będzie poświęcony czwarty wpis z serii DI.

03.12.2009 at 4:40 po południu
Świetny artykuł, napisany w sposób przyjazny użytkownikowi:) Kiedy kolejna część?