niedziela, 29 listopada 2009

Seam == JSF + EJB ?

WSTĘP

Framework Seam opiera się na bardzo silnym mechanizmie - bijekcji.

Bijekcja to nowa jakość w technice Dependency Injection, która pozwala na budowanie bardziej naturalnych konstrukcji Inversion on Control.

/**
 * Na marginesie dodam, że DI i IoC to nie jest to samo.
 * Wyjaśnienie różnic w tym przydługim poście:
 * Wprowadzenie do wstrzykiwania zależności i Springa zarazem
 */

Bijekcja w skrócie przedstawia się następująco:
- podczas wywołania metody
- następuje wstrzyknięcie zależności do obiektu, którego metoda jest wołana (pola adnotowane @In)
- metoda jest wykonana
- następuje wystrzyknięcie obiektów do kontekstu Seam (pola adnotowane @Out)
- następuje czyszczenie (nulllowanie) wstrzykniętych zależności

Od strony technicznej odbywa się to w Seam dzięki specjalnym interceptorom. Przechwytują one wywołania metod wszystkich komponentów i wokół tych wywołań dodają całą opisaną powyżej magię.

Koncepcja jest jak już napisałem piękna, ponieważ pozwala na budowanie dużo bardziej naturalnych konstrukcji. Wstrzyknięcia następują per wywołanie metody a nie jedynie raz, po stworzeniu komponentu - zaspawane na wieki wieków (albo przynajmniej do restartu servera z powodu zwisu aplikacji;)
Dodatkowo niemal bez ograniczeń możemy wstrzykiwać w siebie komponenty o różnych zasięgach - nie ma ograniczenia szerszy zasięg w węższy. Dlatego, że wstrzyknięcie następuje jedynie na czas wywołania metody i nie będziemy mieli nigdy problemu z nieświeżą instancją komponentu.


PROBLEM

Twórcy frameworka poszli jeszcze dalej...
Umożliwiają używanie Sesyjnych EJB wprost w JSF - bez potrzeby pośredniej warstwy Managed Beanów (lub ich specyficznej odmiany Backing Beanów). Czyli komponenty Seam, które są jednocześnie komponentami EJB będą widoczne w kontekście JSF.
Managed Beany są podobno zbędą warstwą, która tylko przeszkadza. Możliwość wołania EJB z JSF jest rzekomo tryumfem rozumu na platformą korporacyjną.

Hmmm w bardzo prostych systemach klasy "Przeglądarka Bazy Danych" rzeczywiście można by się obyć bez MB, ale w niniejszym poście przedstawię do jakich kuriozalnych konstruktów dochodzi gdy w nietrywialnych przypadkach wiążemy JSF wprost z EJB.

Na wstępie zaznaczam, że na potrzeby niniejszych rozważań pomijamy aspekty warstwowej architektury, elementarnych zasad projektowania mówiących o kohezji klasy i temu podobnych staromodnych ograniczeniach. Skupiamy się na "produktywnym" kodowaniu na wyścigi rodem z najlepszych tutoriali i książek.

Zaczynamy.
Przykład prosty, klasyczny ekran prezentujący listę czegoś.
Wymagania:
Po ordynarnym wejściu na stronę przez GET chcemy aby lista wyświetlała wszystkie dane.
Widok ma pozwalać również na wyszukanie czegoś po jakimś atrybucie - czyli wpisanie szukanego słowa w pole tekstowe i naciśnięcie buttonu z zaokrąglonymi rogami "Szukaj" (POST gwoli ścisłości).




Na początek implementujemy pierwsze wymaganie: po wejściu na stronę widzimy listę wszystkiego...

Strzępek kodu widoku:

<h:dataTable value="#{listaCzegos}" var="_cos">
<h:column>#{_cos.nazwa}</h:column>
...
</h:dataTable>


Stateless Session Bean, który dostarcza danych dla widoku:


@Stateless
@Name("cosProwajder")
public class CosProwajderBean implements CosProwajderLocal{
@Out
private List listaCzegos;

@Factory("listaCzegos")
public void initListaCzegos(){
listaCzegos = //pobranie danych
}
}


Co się tutaj dzieje: JSF rząda komponentu listaCzegos. Seam widzi, że nie istnieje on w kontekście, ale na szczęście znalazł ochotnika, który go sfabrykuje - metodę otagowaną adnotacją @Factory("listaCzegos"). Metoda zostaje wywołana, metoda ustawia pole prywatne, a ponieważ pole jest adnotowane @Out to po chwili jest wystrzykiwane do kontekstu. Dzięki temu JSF "widzi" listę i może ją już teraz spokojnie renderować w tabelce.

Kod Session Beana mógłby równie dobrze wyglądać tak:

@Stateless
@Name("cosProwajder")
public class CosProwajderBean implements CosProwajderLocal{

@Factory("listaCzegos")
public List initListaCzegos(){
listaCzegos = //pobranie danych
return listaCzegos;
}
}


Ale już śpieszę wyjaśnić skąd poprzednia konstrukcja. Mam już w zamyśle spełnienie drugie wymagania - funkcjonalności wyszukiwania. Zatem na widoku pojawi się pole tekstowe i button:


<h:form>
<h:inputText value="#{searchFilter.name}" />
<h:commandButton action="#{cosProwajder.search}" />
<h:form>


Nasz Session Bean dostanie metodę search, która wyszuka dane na podstawie wstrzykniętych kryteriów, następnie wynik ustawi w prywatnym polu, z którego to po chwili wartość zostanie wystrzyknięta do kontekstu Seam, skąd JSF będzie ją widział.


@Stateless
@Name("cosProwajder")
public class CosProwajderBean implements CosProwajderLocal{
@Out
private List listaCzegos;

@In
private SearchCriteria searchCriteria;

@Factory("listaCzegos")
public void initListaCzegos(){
listaCzegos = //pobranie danych
}

public void search(){
listaCzegos = //pobranie danych na podstawie searchCriteria
}
}


Jeszcze tylko dla ścisłości komponent przechowujący kryteria wyszukiwania. Zasięg PAGE aby kryteria były widoczne po przeładowaniu strony:

@Name("searchCriteria")
@Scope(PAGE)
public class SearchCriteria implements Serializable{
private String name;
//getter i setter
}



Pytanie: co jest nie tak z poniższym kodem?

@Stateless
@Name("cosProwajder")
public class CosProwajderBean implements CosProwajderLocal{
@Out
private List listaCzegos;

@In
private SearchCriteria searchCriteria;

@Factory("listaCzegos")
public void initListaCzegos(){
listaCzegos = //pobranie danych
}

public void search(){
listaCzegos = //pobranie danych na podstawie searchCriteria
}
}


Dla ułatwienia wyrzucę linijki odwracające uwagę i zaznaczę kluczowy element:
(To przecież nie jest egzamin na certyfikat - chcemy się tu dowiedzieć czegoś pożytecznego)


@Stateless //<<-------------
public class CosProwajderBean implements CosProwajderLocal{
private List listaCzegos;

private SearchCriteria searchCriteria;

public void search(){
listaCzegos = //pobranie danych na podstawie searchCriteria
}
}


Właśnie!
Niby mamy bezstanowy komponent, ale korzystamy z niego w stanowy sposób!

Wyobraźmy sobie, że nasz wspaniały komponent biznesowy jest tak genialny, że chcemy go wykorzystać jeszcze gdzieś poza JSF, np wywołać zdalnie. Musimy wówczas:
1. ustawić kryteria wyszukiwania (zakładając, że mamy setter)
2. odpalić metodę search(), która zmieni stan - tu jest ta nieszczęsna stanowość
3. odebrać wynik przez getter

Czyli JSF wymusza na nas styl "strzelania z muszkietu": załaduj i wypal.

Czy dałoby się wykorzystać mimo wszystko ten super-kod poza Seam, np przykrywając go fasadą:


@Stateless
public class CosProwajderFacadeBean implements CosProwajderFacadeRemote{
@Ejb
private CosProwajderLocal cosProwajder;
public List search(SearchCriteria searchCriteria){
cosProwajder.setSearchCriteria(searchCriteria);
cosProwajder.search();
return cosProwajder.getListaCzegos();
}
}

Niestety NIE, ponieważ nie mamy gwarancji, że kontener JEE przy każdym z 3 wywołań bezstanowego komponentu zaserwuje nam tą samą instancję!


Dlaczego ten bezsensowny kod działa w ogóle w Seam? Tak jak napisałem na wstępie - interceptory Seam. Jeden z nich przechwytuje wołanie metody search na komponencie, wstrzykuje kryteria, wykonuje metodę, wystrzykuje wynik. Ponieważ wstrzykiwanie i wystrzykiwanie nie są wywołaniem metod biznesowych bezstanowego komponentu to interceptor cały czas operuje na tej samej instancji.


ROZWIĄZANIE

Łatwo można rozwiązać problem sensowności kodu zmieniając bean bezstanowy na stanowy. Jednak wciąż mamy problem z bezsensownością logiczną. Dlaczego jakiś komponent będący de facto wrapperem dla procedury ma być stanowy?

Owszem w pewnych sytuacjach stanowość może mieć sens, np: z przyczyn wydajnościowych komponent stanowy trzyma wynik jako jakiś kursor po stronie bazy. Klient Stanowego Komponentu Sesyjnego przegląda listę wynikową po kawałku. Tę argumentację zaliczam.

Innym usprawiedliwieniem może być chęć wybrania (kliknięcia) wiersza - z technicznych powodów musi wówczas przechować listę. Innym jeszcze usprawiedliwieniem może być naiwna paginacja, która w naiwnych paginatorach działa na danych sesyjnych pobranych w całości z bazy.
Czy jednak sensowne jest dopasowywanie API komponentów biznesowych do takich szczegółów technicznych jakiś frameworków prezentacji?
Poza tym wciąż będziemy mieli kuriozalne korzystanie z niego w fasadzie - w stylu "strzelania z muszkietu": załaduj i wypal.


PRAWDZIWE ROZWIĄZANIE

Aby nasze komponenty biznesowe miały sensowny interfejs w nietrywialnych przypadkach musimy ponieść ten niesamowity trud wprowadzenia warstwy jakiś Managed Beanów - w Seam zwanych Akcjami.

Są to zwykłe POJOs, które mają API w stylu "strzelania z muszkietu" a logikę biznesową delegują do EJB:


@Name("cosControler")
public class CosControler{
@In //EJB
private CosProwajderLocal cosProwajder;

@Out
private List listaCzegos;

@In
private SearchCriteria searchCriteria;

@Factory("listaCzegos")
public void initListaCzegos(){
//używamy pustych kryteriów (w celu optymalizacji można by wynieść je do singeltona)
listaCzegos = cosProwajder.search(new SearchCriteria());
}

public void search(){
//Wywolanie EJB
listaCzegos = cosProwajder.search(searchCriteria);
}
}


Jest to również doskonałe miejsce do wstrzyknięcia np kontekstu FacesMessages, kontekstów Seam, parametrów Request, bindowanie UIComponent i innych zależności typowych dla technikaliów frameworków. W tej warstwie możemy sobie na to śmiało pozwolić i dzięki niej nie musimy brudzić EJB zależnościami od Seam i JSF.

//==========================

Sama możliwość wołania Sesyjnych EJB z JSF jest oczywiście bardzo wygodnym ficzerem i warto czasem z niej korzystać. Ale tylko wówczas gdy ma to sens i jest racjonalnie uzasadnione.

Tworzenie komponentu biznesowego, który jest "zbrukany" stylem i zależnościami pewnych technologii powoduje, że nie jest to już ani komponent ani biznesowy. Komponent - czyli pewna reużywalna część; biznesowy - czyli zajmujący się jedynie logiką biznesową.

9 komentarzy:

Damian Łukasik pisze...

1. Ta nowa jakość w DI niedługo odejdzie. Seam 3 ma się opierać na statycznym wstrzykiwaniu proxy przy inicjalizacji, które będzie dynamicznie wyszukiwać konkretne instancje w runtime'ie - czyli BijectionInterceptor przejdzie do historii, co tak naprawdę powinno wywoływanie metod komponentów Seamowych. :)
2. Super, że zwróciłeś uwagę na ułomność promowania bezpośredniego wiązania JSF z EJB. W .NET'cie oczywistą sprawą jest nie umieszczanie logiki aplikacji w klasach Page (odpowiedniki MB), a jedynie delegowanie wywołań, czego twórcy niektórych frejmworków javowych nie mogą zrozumieć. ;]

Sławek Sobótka pisze...

ad1: dzięki za info o proxy.
Muszę zinwestygować temat głębiej, ale póki co wydaje, że semantycznie będzie to wyglądać identycznie. Develoepr nieświadomy mechanizmów frameworka nawet nie zauważy różnicy.

Czy spodziewany jest jakiś przyrost prędkości? Pewnie tak ponieważ odejdziemy od refleksji - chociaż te od Javy5 jest już szybka.

ad2: Heh mnie też zawsze to irytuje. Zwykle nie trzeba tak projektować arch. To, że framework pozwala nie znaczy, że muszę tak postępować.

Ale co mają zrobić juniordeveloperzy gdy widzą takie "wzorce" w tutorialach?

Zastanawiam się też co jest przyczyną a co jest skutkiem promowania takich podejść? Czy twórcy frameworków rzeczywiście nie mogą tego zrozumieć (nigdy nie tworzyli większego systemu i nigdy nie widzieli do jakich problemów to prowadzi)? Czy może dopasowują produkt do odbiorców?

Jeżeli system jest rzeczywiście prościutki to powinno się go implementować w PHP zamiast wytaczać na niego korporacyjną platformę:)

Damian Łukasik pisze...

Ad.1. Poprawa szybkości "powinna" być, gdyż nie będzie wykonywana bijekcja wszystkich zależności za każdym razem, a jedynie lookup przez proxy tych które są wykorzystywane w danym wywołaniu metody komponentu.
Ad.2. Takie tutoriale mają zaletę przy uczeniu się konkretnych technologii. Tą drogą projektowania nigdy się nie da nauczyć. Najlepiej zacząć pracę i przekonać się na własnej skórze o co chodzi i.. duuuuużo czytać jak się powinno projektować systemy. ;)

Sławek Sobótka pisze...

ad 1: ahhh no tak... nie zauważyłem najważniejszego:
pozyskiwanie jedynie potrzebnych zależności.

Jest to rzeczywiście problem, ale wynika on przede wszystkim ze złego dizajnu.

Nie raz widzę "boskie klasy", które mają po kilkanaście metod, każda z metod potrzebuje kilku współpracowników (niby mamy delegację, fajnie). Ale w sumie "boska klasa" ma kilkadziesiąt zależności. Do wykonania konkretnej metody potrzeba zaledwie kilku. Tak jak zauważyłeś problem od strony technicznej polega na tym, że framework "męczy się" z wstrzyknięciem wszystkiego.

Natomiast prawdziwy problem polega na złej ziarnistości. Często robi się "boską klasę" akcji per ekran. A ekran ma kilkanaście buttonow. Brak tu abstrakcyjnego myślenia - oddzielenia widoku od tego co jest potrzebne do pracy.

2. Zgadzam się. Tylko, że z tego co obserwuję to zwykle brakuje czasu/chęci/świadomości o tym, że oprócz technikaliów ważniejsze są aspekty projektowania. Wówczas jedyna lektura na temat projektowania to te nieszczęsne tutoriale radośnie rozwiązujące problemy klasy "hello world".

Ekstrapolowanie architektury "hello world" na duży system wiemy jak się skończyć musi:)

Damian Łukasik pisze...

Więc wszystkie tutoriale powinny posiadać disklejmer: "Rób to tylko w domu". :)))

Marek Dominiak pisze...

@Sławek "Czy twórcy frameworków rzeczywiście nie mogą tego zrozumieć (nigdy nie tworzyli większego systemu i nigdy nie widzieli do jakich problemów to prowadzi)? Czy może dopasowują produkt do odbiorców?"

Wydaje mi się że jest związane z tym że twórcy frameworka chcą jak najmocniej kopnąć w jajka młodych programistów żeby zaczęli tego frameworka używać ;-) Po prostu trzeba taki framework sprzedać, a jeśli HelloWorld z tutoriala wykonuje się 2 godziny i mało osób będzie tego chciało używać (może lepiej powiedzieć mniej osób), za to jeśli takie HelloWorld jest proste jak drut - jedna klaska to wtedy to przemawia do początkującego programisty.

Takie tutoriale mają uczyć jedynie ficzerów takiegoż to frameworka, a nie architektury.

Jeśli nawet wybaczam twórcom frameworków to że tutorial robi coś brzydko (prostacko) to nie mogę wybaczyć tego że nie ma tam wielkiego czerwonego napisu informującego że to tak się nie powinno robić i jakiegoś linku do prawdziwego przykładu.

Ja widziałem raz boską klasę z 56 zależnościami do innych klas (i zaznaczam że nie była to żadna fasada ukrywająca komunikację z jakąś biblioteczką ;-)

Sławek Sobótka pisze...

czerwony disklejmer... jestem za

ale dla każdej technologii byłoby to samobójstwo marketingowe;P

Unknown pisze...

Król jest nagi!

Wstrzykiwanie pól tylko na czas wywołania metody?? Sorry, to jest rozwiązywanie nie tego problemu, co trzeba. Najpierw lepiej by naprawić cykle życiowe obiektów, żeby te krótko-żyjące miały referencje do podobnych lub bardziej długowiecznych (i nigdy na odwrót). Polecam teksty człowieka, którego wysoko cenię, współczesnego głosiciela prawdziwego OOP:

http://misko.hevery.com/2009/04/15/managing-object-lifetimes/

Ale: "każden lubi co innego", a pewnych rzeczy nie da się naprawić we frameworkach opartych o standardy JEE, które są zorientowane obiektowo (tylko że inaczej)

Sławek Sobótka pisze...

oodventurer - na prawdę dobry komentarz, dzięki.

1. Rzeczywiście sensowność zależności i ich zasięgu jest problemem - o ile ktoś się nad tym zastanawia:)

Problem bierze się np z niemożliwości przekazywania parametrów do metod JSF (standard JEE). Sam JSF nie pozwoli na wstrzyknięcie requestowego obiektu do sesyjnego. Ale w Seam możesz już zrobić prawie wszystko - dlatego powinna być to zabawka dla dużych chłopców, którzy wiedzą co robią. Marketing jednak chce to po prostu sprzedać komu się da;P

Poza tym często liczy się jedynie to aby działało, utrzymaniem będziemy się martwić później. Sad but true.

2. Post na blogu, który podałeś podaje racjonalne argumenty. Chociaż sama koncepcja jest w sumie oczywista, jednak trzeba jakoś sobie radzić w zastanym, nieidealnym świecie JEE.

W wolnej chwili przejrzę głębiej tego bloga - widzę, że jest sensowny. Gdybyś mógł to podaj parę linków do najlepszych postów.

3. Obiektowe zorientowanie JEE... wszyscy wiem jak z tym jest (tzn jeżeli ktoś czuje pure OO). Z wierzchu dominuje "object wrapper" - opakowanie dla procedur, ponieważ język ma składnię jaką ma i procedurki nie mogą się walać po kodzie tak obie nieprzypięte do klasek.

ale

zastanówmy się czy platforma korporacyjna musi być OO?
- nie każda domena jest obiektowa, a nawet jeżeli jest to nie każdy potrafi ją tak zamodelować
- pure OO jest trudne, nie każdy to czuje. A technologia jest mainstremowa więc musi trafiać do jak najszerszego grona developerów.