Prezentacja była skierowana z założenia do początkujących, jednak aby nie zanudzić bardziej zaawansowanych zawiera ona również parę zagadnień dla nich - zostały one oznaczone czterolistną koniczynką (żeby pozostać w wiosennym klimacie). Zatem jeżeli jesteś początkujący i w pewnym momencie nie rozumiesz o co chodzi a na slajdzie widnieje koniczynka wówczas wszystko jest ok.
Celem posta jest przedstawienie ogólnej idei Inversion of Control (IoC) oraz jej szczególnej reifikacji (trudne słowo) zwanej Dependency Injection (DI). Jest to wg mnie bardzo istotne na początek ponieważ duża część nowszych frameworków zasadza się na tych fundamentach. Później możemy przejść do konkretnego przykładu w Springu. Przeglądając różne tutoriale stwierdzam brak wyjaśnienia o co w ogóle chodzi z IoC czy DI. Pokazuje się jedynie jakieś autystyczne przykładziki bez wyjaśnienia sedna problemu - po co i na co mam coś wstrzykiwać, co mi to daje i kiedy powinienem to robić.
Ludzie podobno najlepiej uczą się na przykładach. Owszem - do nauki prostych czynności typu rozbicie kokosa kamieniem wystarczy sam przykład; jednak jak przedstawić ideę na samym przykładzie? Albo inaczej: jaka ilość przykładów pozwoli na drodze logicznego myślenia wyłonić z nich ideę?
Zaczynamy.
Zgodnie z agendą przejdziemy po niektórych slajdach...
Na początek wymyślimy sobie problem. Problem oczywiście będzie polegał na tym, że pojawiają się zależności i nie wiemy jak sobie z nimi poradzić. Później nastąpi krótki przegląd możliwych rozwiązań. Wszystkie one są oczywiście złe:) Dalej całkiem przypadkiem wpadniemy na koncepcję rozwiązania dużo lepszego. Poznamy ogólną koncepcję IoC i DI. Teraz jesteśmy już gotowi na zapoznanie się z przykładową implementacją w Springu. Następnie przedstawię zarówno zalety jak i ograniczenia wstrzykiwania zależności. Na zakończenie poszerzymy nieco horyzonty... Spring to nie tylko wstrzykiwanie zależności.
KONTEKST PROBLEMU
Na potrzeby naszych rozważań próbowałem wymyślić jakiś ekscytujący problem biznesowy. Niestety bezskutecznie. Dlatego musimy się zadowolić wyświechtanym przykładem z zamówieniem. Zatem załóżmy, że mamy zamówienie, które musimy złożyć. Nasz proces składania zamówienie niech będzie trywialny: wyliczamy podatek i zapisujemy zamówienie.
Zakładam, że kombinujemy w stronę Object Oriented oraz, że chcemy zaprojektować nasze rozwiązanie zgodnie z GRASP.
Zgodnie ze standardowym (choć nie koniecznie najlepszym) tokiem myślenia powinniśmy dość do powyższego projektu. W centrum mamy encję biznesową Order. Nie chcemy aby była ona anemicznym modelem więc przykładowo przypisujemy jej metodę biznesową wliczającą wartość tegoż zamówienia. Dla przejrzystości pomijamy szczegóły takie jak atrybuty zamówienia (data, status itp) oraz jego wewnętrzną strukturę (kolekcję produktów) ponieważ nie wnoszą one nic w kontekście wstrzykiwania zależności.
Logika aplikacji (logika przepływu, use case, ogólnie - nie biznesowa) jest zhermetyzowana w serwisie. Logika ta jest prosta, polega na wyliczeniu podatku i zapisaniu zamówienia. Ponieważ chcemy podążać zgodnie z OO to aspekt podatku i zapisu został wydzielony (enkapsulacja). Oba aspekty mogą mieć różną postać dlatego zostały przykryte stabilnym interfejsem.
PROBLEM
Ok, nasza logika aplikacji zależy od 2 rzeczy: polityka wyliczania podatku i sposób zapisu zamówienia. Pytanie: skąd je wziąć?
Niektórzy się tym nie przejmują i radośnie hardcodują typy w kodzie. Tzn usztywnianie kodu następuje dużo wcześniej. Po co w ogóle wydzielać jakieś klasy:P
Gdy jednak projektant przyciśnie i mamy już hermetyzację oraz wymaganie co do możliwości zmiany implementacji pojawia się pomysł jej wyboru przy pomocy kaskady ifów albo słicza.
Dalej nauczeni doświadczeniem z problemami słiczy zmian w setkach miejsc enkapsulujemy je do jakiejś fabryczki. Koncepcja ta może wyewoluować nawet do całkiem zgrabnego rozwiązania opartego o Abstract Factory, który zaczytuje konfigurację np z XML.
Jako uzupełnienie wspomnę tylko rozwiązanie, które widziałem u sprytnego mistrza .NET: dyrektywy kompilacji - bez komentarza.
O ile rozwiązanie oparte o fabrykę abstrakcyjną jest już całkiem w porządku to ma jednak wadę estetyczną. Kod biznesowy czy jakikolwiek inny jest zbrukany jakimiś dziwnymi odwołaniami do technicznych obiektów fabryk. Fuj.
Skoro odrzucamy myśl o tym, że obiekt zajmuje się zdobywaniem swych zależności to nie pozostaje nic innego jak zapodawanie ich z zewnątrz.
POMYSŁ NA ROZWIĄZANIE
Jeżeli obiekt nie jest odpowiedzialny za tworzenie czy zdobywanie swych zależności lecz dostaje z kątowni to właśnie odkryliśmy ideę Inversion of Control!
W standardowym podejściu nasz kod korzysta z już istniejących bibliotek (nawet gdyby była to klasa napisane minutę temu) w celu nadbudowania nad nimi nowej struktury. Nasz kod krok po kroku wywołuje biblioteki. W IoC jest odwrotnie - to kod biblioteczny wywołuje nasz własny kod - wymaga to zmiany sposobu myślenia. Nie programujemy imperatywnie lecz deklaratywnie aby coś zbudować. Konstrukcja już istnieje lecz brakuje w niej pewnych szczególnych komponentów realizujących szczegółowe funkcje. Można to sobie wyobrazić w ten sposób, że kod biblioteki zajmuje się kompleksowym rozwiązaniem problemu jednak w pewnym miejscach woła kod klienta. To jaki kod klienta biblioteki ma być podłączony musimy właśnie w jakiś sposób zadeklarować (podłączony - chciało by się powiedzieć wstrzyknięty, ale nie uprzedzajmy faktów).
W naszym przykładzie servis składający zamówienie jest powiedzmy biblioteką. Załóżmy, że ktoś stworzył doskonałą bibliotekę do składnia zamówień, ale oczywiście nie wiedział jak zaimplementować wyliczanie podatku i zapis zamówienia. Dlatego zostawił to w gestii kodu klienta biblioteki. Jeszcze raz podkreślę różnicę: kod klienta nie woła biblioteki - jest na odwrót. Jest to podejście charakterystyczne dla frameworków.
Ogólnie można powiedzieć, że IoC jest podejściem do projektowania architektur.
PRÓBA ROZWIĄZANIA W NOWYM STYLU
Skoro nasz pomysł z odwróceniem kontroli nie jest objawem schorzenia, ponieważ jest opisany nawet na wiki, to spróbujmy może tak:
Skoro nasz servis potrzebuje załadowania go zależnościami przed rozpoczęciem pracy to wychodzi chyba na to, że składanie powinno nastąpić gdzieś w interfejsie użytkownika.
Nic bardziej mylnego!
UI służy do różnych dziwnych rzeczy ale na pewno nie powinno zajmować się decydowaniem o składaniu komponentów biznesowych. Wiem, że kiedyś w onClick pisało się żywego SQLa, ale czasy Visual Basica czy Delphi już się skończyły. Co prawda niechlubną tradycję podtrzymuje Seam, ale normalnie warstwa prezentacji służy do hermetyzowania logiki prezentacji (ot niespodzianka) - czyli np decydowania czy z uwagi na imieniny usera narysować kwiatek czy butelkę whisky.
POTRZEBUJEMY WSTRZYKIWANIA ZALEŻNOŚCI
Idea wstrzykiwania zależności jest prosta jak budowa cepa. Musi istnieć jakiś byt (zwany zazwyczaj kontenerem IoC), który ma dostęp do konfiguracji i bibliotek klas. Byt ten poproszony o jakiś obiekt szuka jego definicji w konfiguracji. Na podstawie konfiguracji jest w stanie określić jakiej klasy powinien być żądany obiekt oraz jakie ma zależności. Zależne obiekty są w razie potrzeby tworzony po czym są wstrzyknięte (po prostu wstawione) do obiektu żądanego. Oczywiście same obiekty zależne mogą same mieć własne zależności - kontener również dla nich przeprowadzi rekursywnie opisaną procedurę.
Uwagę na powyższym slajdzie przykuwa zapewne pękaty słoik. Istotne jest, że kontener pracuje na POJOs. Czyli DI nie wymaga uzależniania naszego kodu od jakiś dziwnych interfejsów. Nie wymaga dziedziczenia po ezoterycznych klasach bazowych. Kod jest czysty ponieważ zajmuje się tym czym powinien.
WYJAŚNIENIE
Zanim przejdziemy do przykładu, na który już pewnie wszyscy czekają z niecierpliwością, chcę wyjaśnić jedną kwestię...
Pojęcia IoC i DI są nagminnie używane zamiennie. IoC i DI nie są synonimami. Di jest jednym ze sposobów na osiągniecie IoC. Owszem najczęściej realizujemy IoC przez DI ale to nie znaczy, że można je utożsamiać.
PRZYKŁAD WSTRZYKIWANIA ZALEŻNOŚCI PRZEZ SPRING IoC
Na potrzeby przykładu zaimplementowałem projekt przedstawiony wcześniej w UML. Struktura pakietów odpowiada standardowym warstwom, więc mamy kolejno (wg"wysokości" a nie wg sortowania przez drzewko pakietów w Eclipse):
- ui - interfejs użytkownika. Żałosny ten przykładowy interfejs, ale działa;P
- logic.application - zawiera logikę naszej apliakacji, czyli konkretny Use Case - usługę składania zamówienia (jest to podejście proceduralne, ale co tam - na potrzeby tego przykładu wystarczające)
- logic.business - model biznesu, mamy tu dwie standardowe figury z Domain Driven Desing: Encję oraz Politykę. Polityki są przykryte stabilnym interfejsem ponieważ zakładamy, że są polimorficzne. Polityka to nic innego mój ulubiony wzorzec projektowy - Strategy.
- dao - zawiera interfejs OrderDAO i jego przykładową implementację opartą na XML
Przyjrzyjmy się bliżej klasie OrderServiceImpl z pakietu logiki aplikacji. Jej trywialna metoda biznesowa makeOrder jedyne co robi to skorzystanie z polityki podatkowej w celu wyliczenia podatku i z DAO w celu zapisania zamówienia.
@Override
public void makeOrder(Order order) {
taxPolicy.calculateTax(order);
orderDAO.saveOrder(order);
}
Metoda ta korzysta z prywatnych pól.
private OrderDAO orderDAO;
private TaxPolicy taxPolicy;
Skąd wezmą się ich wartości? Oczywiście zostaną wstrzyknięte przez kontener IoC Springa. W jaki sposób? Poprzez settery:
public void setOrderDAO(OrderDAO orderDAO) {
this.orderDAO = orderDAO;
}
public void setTaxPolicy(TaxPolicy taxPolicy) {
this.taxPolicy = taxPolicy;
}
Zwróćmy uwagę iż settery występują jedynie w klasie implementacji i nie ma ich w interfejsie. Po prostu to ta konkretne implementacja serwisu potrzebuje do swego sensownego działa tych 2 zależności. Być może inne implementacje nie koniecznie muszą mieć takie same zależności. Dlatego nie zakładajmy nić o zależnościach i nie umieszczajmy setterów w interfejsach.
Cały dramat zaczyna się właśnie w klasie UI nazwanej dumnie ConsoleApplication.
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[]{"application-context.xml"});
BeanFactory factory = context;
OrderService orderService = (OrderService) factory.getBean("orderServiceBean");
orderService.makeOrder(new Order());
I to wszystko. Kolejne linijki odpowiadają za:
- Utworzenie kontekstu Spring. W tym przypadku tworzymy go używające klasy
org.springframework.context.support.ClassPathXmlApplicationContext.ClassPathXmlApplicationContext zajmuje się ona wyszukaniem podanego pliku konfiguracyjnego (który powinien znajdować się na class path). Plik ten zawiera definicję wszystkich zależności; zostanie omówiony za chwilę. Kontekst podczas "wstawania" zaczytuje konfigurację i ewentualnie tworzy obiekty wraz z ich zależnościami. - Rzutowanie kontekstu na fabrykę to jedynie lukier i dobra praktyka. Kontekst posiada wiele metod "technicznych", które nie są potrzebne "zwykłym" kawałkom kodu. Do pobierania obiektów z kontenera wystarczy nam interfejs BeanFactory.
- Pobranie z kontekstu obiektu o zadanej nazwie. Fabryka zwraca Object więc potrzeba rzutowania. W tym miejscu interesuje mnie jedynie interfejs obiektu; z resztą nie mam nawet pojęcia jaka konkretna klasa może siedzieć w konfiguracji.
- Pozostało nam już tylko wykonanie metody biznesowej na pobranym z kontenera obiekcie.
Łatwe, proste i przyjemne.
Gdzie cała magia? Oczywiście w konfiguracji Springa - plik application-context.xml (Przy okazji dodam, że wygodne może być rozbicie konfiguracji na kilka plików; np osobny plik dla każdej z warstw).
CO MISIO MA W ŚRODKU...
Składania XMLa konfiguracyjnego dla Springa jest dosyć intuicyjna.
<?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 id="orderServiceBean" class="org.slawek.project1.logic.application.impl.OrderServiceImpl">
<property name="taxPolicy"><ref bean="polishTaxPolicy"/></property>
<property name="orderDAO" ref="xmlOrderDAO"/>
</bean>
<bean id="polishTaxPolicy" class="org.slawek.project1.logic.business.policies.impl.PolishTaxPolicy">
<constructor-arg type="int" value="22"/>
</bean>
<bean id="xmlOrderDAO" class="org.slawek.project1.dao.xml.XMLOrderDAO"/>
</beans>
Przeanalizujmy plik konfiguracyjny od dołu. Deklarujemy obiekt o nazwie xmlOrderDAO podając konkretną jego klasę. Wyżej podobnie deklarujemy obiekt polishTaxPolicy. Jednak akurat ta polityka zależy od warośći VAT, którą potrzebuje w swym konstruktorze - wstrzyknięcie do konstruktora deklarujemy w tagu constructor-arg.
Na końcu (a właściwie początku) deklarujemy obiekt orderServiceBean - ten sam, który pobieramy z BeanFactory. Obiekt ten zależy od 2 poprzednich - ma nawet settery, które tylko czekają aby im wstrzyknąć jakieś implementacje interfejsów. Wstrzyknięcie poprzez setter deklarujemy z użyciem tagu property.
Spring "widząc" taką konfigurację "wie" czym jest obiekt orderServiceBean (żądany w aplikacji z fabryki) i wie również jak wstrzyknąć jego zależności, które oczywiście same mogą mieć również własne zależności.
INTEGRACJA CAŁKOWITA
Powyższy przykład ma jedną wadę. Najwyższa warstwa aplikacji korzysta z kontekstu Springa. Owszem klasy niższych warstw nie zajmują się już pozyskiwaniem swych zależności, ale niestety nie uniknęliśmy tego w warstwie UI.
Nie wiem jak wygląda to w aplikacjach standalone, ale Spring gładko integruje się z frameworkami webowymi.
W przypadku czystego JSF czy Seam wystarcz w web.xml zadeklarować listenera, który "podniesie" kontekst Springa (wskazując mu ewentualnie lokalizację plików konfiguracyjnych). Później już tylko ustawienie specjalnego resolvera w faces-config.xml i o tej pory w wyrażeniach EL możemy się posługiwać obiektami z kontekstu Springa. W przypadku integracji z Seam możemy nawet deklarować zasięg beanów Springowych tak samo jak Seamowych. Więcej o integracji Seam ze Spring tu.
Ogólnie nie mamy już tej niezręcznej sytuacji polegającej na tym, że najwyższa warstwa jednak zdobywa zależności we własnym zakresie.
SINGLETONY
Domyślnie wszystkie beany w Springu są singletonami (attrybut scope tagu bean). Zmiana jego wartości na prototype skutkuje tym, że wstrzyknięcie referencji spowoduje wstrzyknięcie całkiem nowej instancji.
OGRANICZENIA WSTRZYKIWANIA ZALEŻNOŚCI W SPRINGU (UWAGA - KONICZYNKA)
W odróżnieniu od nowszych frameworków (np Seam) Spring wstrzykuje zależności podczas tworzenia obiektów. Co jeżeli Spring nie będzie odpowiedzialny za stworzenie obiektu - tak jak np w wypadku encji tworzonych przez frameworki ORM? Jeżeli Spring czegoś nie tworzy, to nie może temu czemuś wstrzyknąć zależności - logiczne.
Pewnie niejeden czytelnik zadaje sobie w tym momencie pytanie: o czym on do cholery pisze? Wstrzykiwanie zależności do encji z hibernate? Otóż z godnie z Domain Driven Design encje powinny mieć odpowiedzialność biznesową. W końcu to obiekty a nie jakieś recordy z Pascala albo struct z C.
Jednym ze sposobów na wstrzyknięcie zależności do encji jest Springowa adnotacja @Configurable. Niestety polega ona na weavingu bytecodu podczas ładowania więc praktycznie wyklucza to testowalność.
Zalecanym patentem są interceptory Hibernate. Interceptor może po stworzeniu encji ustawić jej to i owo.
Czyli oficjalne wersja jest taka: do encji wstrzykujemy zależności poprzez interceptory hibernate a wszystko inne jest nakłuwane Springiem. Moim skromnym zdaniem dwa sposoby na wstrzykiwanie zależności to co najmniej o jeden za dużo! Sugeruję aby zamiast rzeźbić po prostu pozbyć się problemu:)
Zamiast wstrzykiwać do encji sugerują aby wstrzykiwać do warstwy wyższej - logiki aplikacji. Scenariusz wyglądał by tak:
1. Coś (np polityka) jest wstrzyknięta do serwisu aplikacyjnego
2. Serwis przy pomocy DAO/Repozytorium pobiera encję
3. Serwis po prostu ustawia zależność na pobranej właśnie encji
Dzięki temu przy tak zwanej okazji dostajemy za darmo użyteczne narzędzie. Możemy z dużym prawdopodobieństwem założyć, że w różnych use case, czyli różnych serwisach aplikacyjnych będą potrzebne np różne polityki. W sugerowanym podejściu nie stanowi to żadnego problemu. Do jednego servisu wstrzykujemy politykę A, do drugiego B. Servis gdy trzeba ustawi swoją (wstrzykniętą) politykę do encji. Natomiast w razie gdybyśmy uparcie dążyli do wstrzykiwania zależności (np polityk) wprost do encji wówczas byłby niemały problem.
ZALETY PŁYNĄCE Z IoC i DI
Inversion of Control skłania do projektowania systemów za zasadzie konfigurowalnych (a co za tym idzie reużywalnych) komponentów. Posługując się Dependency Injection możemy dostarczać klientom różne wersje systemów, które technicznie różnią się jedynie plikami konfiguracyjnymi Springa.
Pamiętajmy jednak, że sam fakt korzystania z kontenera IoC nie zapewnia nam eleganckiej i reużywalnej architektury. Wszystko zależy od ilości i jakości pracy umysłowej włozonej w projekt. DI to jedynie narzędzie do realizacji naszej dobrej lub błędnej koncepcji.
Projektowanie systemu na zasadzie niezależnych komponentów drastycznie zwiększa testability. Idąc dalej możemy przygotowywać specjalne wersje plików konfiguracyjnych spreparowana na potrzeby testów integracyjnych. Na ten przykład w razie zależności od modułu komunikującego się z zewnętrznym systemem możemy na czas testów wstrzyknąć implementację tego modułu, która jedynie symuluje interakcję. Przydaje się to na pewno gdy interakcja jest długotrwała a my chcemy przeprowadzić testy masowe. Oczywiście kontener IoC nie powinien być angażowany do testów jednostkowych.
No więc właśnie...
Wstrzykiwanie zależności to jedynie fundament,na którym stoi framework. Zresztą zobaczmy na architekturę Springa
Oraz na architekturę pełnej aplikacji
Mam nadzieję, że powyższa architektura wygląda zachęcająco do dalszego zgłębiania Springa...
Jak widać Spring stawia na integrację dobrych rozwiązań zamiast na implementację wszystkiego od nowa.
Chociaż gdyby stale nie wymyślano koła od nowa to jeździli byśmy wciąż na drewnianych krążkach.
//========================
Prezentacja do ściągnięcia tu: open office i mikrosoft ofis. Natomiast przykłady zawarte zostały w prostym projekciku Eclipse. Projekcik ma strukturę źródeł zgodną z Maven ale sam nie jest projektem Maven - biblioteki są po prostu skopiowane do katalogu lib.
EDIT
video: część 1, część 2
4 komentarze:
Kolejna świetna prezentacja. Z każdą następną rozkręcasz się i nie odpuszczasz. Musiałeś ją jeszcze rozwinąć. ;) Z niecierpliwością czekamy na następne.
Rozwinięcie na blogu wynika z tego, że po prostu zapomniałem powiedzieć o paru sprawach. Sprawach, które są bardzo istotne a niestety stres i emocje podczas prezentacji powodują, że hipokamp gubi niektóre ścieżki skojarzeń:/
Niedawno "odkryłem" twojego bloga i z zaciekawieniem przedzieram się przez archiwum.
Tyle słodzenia a teraz pytanie natury technicznej:
Weźmy przykład z wstrzykiwaniem implementacji DAO. Sugerujesz wstrzykiwanie bezpośrednio do XXXServiceImpl zamiast zastosowania DAOFactory. Jednak przy większym projekcie tych różnych ServiceImpl będzie sporo, więc będzie też sporo wstrzykiwania. Czy nie lepiej więc zostawić DAOFactory, do którego wstrzykniemy właściwe implementacje a wszystkie ServiceImpl będą sobie pobierać odpowiednie DAO z fabryki?
Pytasz o DAO Factory... Jak dla mnie nie jest to pytanie natury technicznej, ale właśnie natury ideologicznej.
Co by bylo gdybyśmy uzywali fabryki ktora jak zrozumialem ma byc rodzajem boskiej klasy wszechwiedzącej o kazdym DAO w systemie?
- dzialamy wbrew koncepcji Inversion of Control poniewaz kod biznesowy nie zajmuje sie wyłącznie biznesem lecz dodatkowo kontaktuje sie z jakąś fabryką
- fabryka jak wspomnialem ma troche duzo zaleznosci
- co prowadzi do balaganu podczas testow poniewaz musze zaladowac fabryke nie wiem czym przed testem. Normalnie na czas testow serwisu wstrzyknąłbym do niego tylko potrzebne mu MOCK-implementacje DAO
Nie martwil bym sie rowniez o ilosc serwisow do skonfigurowania poniewaz i tak musze je wstrzyknac do warstwy prezentacji.
I jeszcze jeden aspekt - tym razem czysto praktyczny i pragmatyczny (tu zaprzeczam sam sobie): bralem udzial w projekcie, w ktorym radosnie wstrzyknaleismy blisko 100 DAO do klikudziesieciu servisow praktycznie PO NIC! Po nic poniewaz NIGDY nie zmienilismy ZADNEJ implemetacji DAO oraz NIGDY nie robilismy testow jednostkowych serwisow. Byla to czysta sztuka dla sztuki.
Dlatego przyklad z DAO nalezy traktowac jedynie jako przyklad na wstrzykiwanie zaleznosci i nie isc slepo za tutorialem Springa - nie ma sensu nakaldac gaci przez głowę gdy projekt jest prosty/prostacki.
Prześlij komentarz