15 kwietnia 2009

Rozdział 9. o Grails Web Flow z DGG2

Kilkakrotnie już wspominałem o tym, że bodaj najbardziej zaawansowanym rozdziałem w "The Definitive Guide to Grails, Second Edition" (DGG2) jest rozdział 9. o Grails Web Flow (Chapter 9. Creating Web Flows). Rozdział przedstawia sposób, w jaki Grails korzysta ze Spring Web Flow udostępniając nam specjalizowany DSL do tworzenia stanowych interakcji. Moje pierwsze podejście do tego rozdziału skończyło się...szybkim "goto" do kolejnego. Po prostu tyle było dla mnie nowości - nigdy wcześniej nie pracowałem ze Spring Web Flow - że kiedy zaznaczałem kolejne części tekstu do dzisiejszej relacji okazało się, że musiałbym przepisać cały rozdział (!) Skończyło się na kilkukrotnym wertowaniu rozdziału, aby wyłapać to, co najważniejsze. Kiedy przypomnę sobie, jak Maciej Majewski musiał gimnastykować sie z IntelliJ IDEA i tymi wszystkimi springowymi XMLami podczas jego prezentacji Spring Webflow w przykładach na java4people w Szczecinie miałem wrażenie, że gość lubi "jazdę bez trzymanki" (wersja oficjalna - oryginalne stwierdzenie ocenzurowano ;-)). Aż dziw bierze, że nie zniechęcił się. XML tu, XML tam i tak przez okrągłe 1,5h. A co musiało być na początku jego drogi, aby poznać te wszystkie zawiłości?! Brrr. Szczęśliwi Ci, którzy poznają różne technologie przez pryzmat Grailsa, który, wraz z Groovy, sprowadza wszelkie sztuczki XMLowe do własnego DSLa, a z nim z programowania pozostaje jedynie...proza angielska ;-) Praca z XMLem to faktycznie rzemiosło, takie XIX-wieczne, podczas gdy Grails Web Flow DSL wprowadza nas w wiek XXI. Nie twierdzę, że Grails jest panaceum na wszelkie nasze bolączki tworzenia aplikacji webowych, ale poznawanie wykorzystywanych technologii wspomagających, jak np. Spring Web Flow, powoduje, że ich nauka jest przyjemniejsza, podzielona na etapy. Weźmy za przykład wspomniany Spring Web Flow. Z Grails zaczniemy od jego SWF DSL (ang. Spring Web Flow Domain Specific Language) bez konieczności grzebania się z XMLami. Dopiero po tym rekomendowałbym poznawanie trzewi Spring Web Flow i zejście na poziom konfiguracji XML. Najpierw delikatne wprowadzenie w temat SWF z pomocą Grailsa, a dopiero po tym prawdziwy hard core.

Rozdział rozpoczyna się stwierdzeniem, że większość aplikacji webowych jest bezstanowa - zero informacji o dotychczasowych poczynaniach użytkownika, co ma tę zaletę, że nasza aplikacja skaluje się liniowo względem dokładanych serwerów (niekoniecznie dokładanej mocy obliczeniowej pojedynczego serwera) - brak replikacji stanu użytkowników. Oczywiście, nie wszystko daje się zamodelować bezstanowo. Przykładem może być nasza wizyta w sklepie z perspektywy wózka, do którego wkładamy towary. Rozpoczynamy od wkładania towarów, aby później podejść do kasy. Bezpośrednie podejście do kasy nie ma sensu (taki noop), więc jedynie sensowny ciąg akcji to najpierw wędrówka po sklepie, aby dopiero z przynajmniej jednym towarem podejść do kasy. Podobnie z funkcjonalnością typu asystent/pomocnik, które zostały okrzyknięte jako konwersacje webowe (ang. web converstations). Do tego właśnie wykorzystujemy Grails Web Flow.

Spring Web Flow to maszyna stanów, w których znajduje się (przechodzi) nasza aplikacja, aby ostatecznie znaleźć się w stanie końcowym. Za pomocą flowExecutionKey oraz identyfikatora zdarzenia, przekazywanych w żądaniu, przenosimy się ze stanu S1 do stanu S2. Z Grails Web Flow zajmujemy się definicją przepływu (ang. flow) bez angażowania się w niuanse XMLowe.

Przepływ definiowany jest w postaci akcji kontrolera, której nazwa kończy się Flow, np.:
 class UczenController {
def zestawTrzechPytanFlow = {
...
}
}
Obowiązkowy identyfikator przepływu (ang. flow id) to nazwa akcji bez kończącego "Flow" (w naszym przypadku będzie to odpowiedz).

W przeciwieństwie do akcji, treść (ciało) domknięcia-przepływu nie zawiera logiki biznesowej, a jedynie sekwencję stanów. Poszczególne stany są wywołaniami metod, których parametrem wejściowym jest domknięcie.
 class UczenController {
def zestawTrzechPytanFlow = {
pytanie1 {
...
}
}
}
Pierwszy stan to stan początkowy przepływu.

Stan z widokiem (chciałoby się powiedzieć "perspektywiczny", ale to na pewno nie oddałoby sensu takiego stanu) to stan, który zatrzymuje przepływ, aby wyświetlić stronę. Strony przepływów znajdują się w grails-app/views/[kontroler]/[identyfikatorPrzepływu]/[stan].gsp (różnica w stosunku do regularnych akcji, to umiejscowienie stron w podkatalogu o nazwie odpowiadającej identyfikatorowi przepływu).

Z każdą akcją przepływu definiujemy sposób obsługi zdarzenia, które zazwyczaj przeprowadzają przeływ do nowego stanu. W ramach treści akcji wywołujemy metodę on z parametrem będącym nazwą zdarzenia. Wykonując metodę to określamy, do jakiego stanu przechodzi nasz przepływ.
 class UczenController {
def zestawTrzechPytanFlow = {
pytanie1 {
on("poprawnaOdpowiedz").to "pytanie2"
on("niepoprawnaOdpowiedz"). to "pomoc"
}
}
}
Taki typ programowania, gdzie tworzy się łańcuch wywołań metod na podstawie wyniku wykonania poprzedniej metody, ma nawet swoją nazwę - "fluent API"!

Stan końcowy to stan, który nie przyjmuje argumentów, albo przekierowuje (ang. redirect) do innej akcji bądź przepływu.
 class UczenController {
def zestawTrzechPytanFlow = {
pytanie1 {
on("poprawnaOdpowiedz").to "pytanie2"
on("niepoprawnaOdpowiedz"). to "pomoc"
}
ocena()
wpisanieOcenyDoDziennika {
redirect(controller: "dziennik")
}
}
}
Stan ocena powoduje zakończenie przepływu i wyświetlenie strony grails-app/views/uczen/zestawTrzechPytan/ocena.gsp, natomiast stan końcowy wpisanieOcenyDoDziennika po prostu przeniesie nas do kontrolera dziennik (z jednoczesnym wykonaniem domyślnej akcji).

W ramach dowolnej akcji stanu możemy wyświetlić inną niż domyślną stronę z pomocą znanej już nam metody render, np.:
 def zestawTrzechPytanFlow = {
pytanie1 {
render(view: "pytanie")
on("poprawnaOdpowiedz").to "pytanie2"
on("niepoprawnaOdpowiedz"). to "podpowiedz"
}
...
}
Między stanami początkowym i końcowym może wystąpić kilka innych stanów. Wyróżniamy stany z widokiem i stan akcyjne (ang. action state). Stan akcyjny to stan, który nie oczekuje akcji po stronie użytkownika, a jedynie wykonuje pewną własną akcję jako wywołanie metody action z domknięciem, która to określa potencjalne przejście do nowego stanu.
 class UczenController {
def zestawTrzechPytanFlow = {
pytanie1 {
on("poprawnaOdpowiedz").to "pytanie2"
on("niepoprawnaOdpowiedz"). to "podpowiedz"
}
ocena()
wpisanieOcenyDoDziennika {
redirect(controller: "dziennik")
}
podpowiedz {
action {
[ odpowiedz: Odpowiedz.find(idPytania) ]
}
on("success").to "pytanie2"
on(Exception).to "drapaniePoGlowie"
}
}
}
Wynik działania metody action może określić model przepływu i dostępny jest w czasie jego trwania. W naszym przypadku modelem będzie mapa z kluczem odpowiedz. Brak błędów wykonania akcji stanu akcyjnego to zgłoszenie zdarzenia success (stąd jego obsługa w on("success") i przejście do stanu pytanie2). Przechwycenie wyjątku to po prostu zdefiniowanie metody on z parametrem, który odpowiada jego typowi. W powyższym przykładzie przechwytujemy wszystkie wyjątki dziedziczące po java.lang.Exception.

Stany akcyjne mają możliwość zgłoszenia dowolnych zdarzeń z poziomu action. Wystarczy wywołać metodę bezparametową, której nazwa określa nazwę zdarzenia.
 def zestawTrzechPytanFlow = {
rodzajPytan {
action {
params.symbolePierwiastkow ? tak() : nie()
}
on("tak").to "seriaPytanZSymboli"
on("nie").to "seriaPytanZNazw"
}
...
}
Tak na prawdę to wynik akcji określa zdarzenie, więc w konstrukcjach if-else będziemy korzystać z return, np.:
 action {
if (params.symbolePierwiastkow)
return tak()
else
return nie()
}
Podobno, od Groovy 1.6, można zapisać powyższy przykład bez return - po prostu same nazwy zdarzeń w postaci bezparametrowych metod.

Z przepływami związane są specyficzne dla nich przestrzenie widoczności (istnienia) obiektów. Poza request oraz session mamy do dyspozycji flash, flow oraz conversation. Wszystkie przestrzenie są mapami i jedyną różnicą w ich działaniu jest czas ich dostępności.

Przestrzeń flash umożliwia przechowywanie obiektów dla aktualnego i następnego (wyłącznie jednego) żądania, flow przechowuje obiekty tak długo, aż zakończy się przepływ, tj. przepływ przejdzie w stan końcowy lub wygaśnie, natomiast przestrzeń conversation przechowuje obiekty dla przepływu i jego przepływów potomnych. Obiekty umieszczone w przestrzeniach przepływowych muszą implementować java.io.Serializable (istotna różnica między flash w zwykłych akcjach a tych z przepływu). Możemy wyłączyć przechowywanie obiektów (poza pamięcią) przez parametr grails.webflow.flow.storage w grails-app/conf/Config.groovy z wartością client. W ten sposób utrzymujemy stan przepływu jako wartość flowExecutionKey. Składowe obiektów przechowywanych w przestrzeniach, które nie powinny być serializowane musimy oznaczyć jako transient (pamiętajmy o akcjach-domknięciach, które nie implementują Serializable, a jako zmienne uczestniczą w serializacji - trochę dla mnie trudne do zrozumienia, co miałoby być problemem, ale skoro się to podkreśla warto o tym chociażby wspomnieć i pamiętać).

Istnieją dwa sposoby, aby spowodować wystąpienie zdarzenia - poprzez link albo formularz.

Pierwszy sposób - przez link - korzysta z g:link i zamiast action wskazującej na zwykłą akcję, wskazujemy nim na przepływ przez jego identyfikator, np.:
 <g:link controller="uczen" action="zestawTrzechPytan">
Podejdź do Zestawu Trzech Pytań
</g:link>
W ten sposób rozpoczynamy przepływ. Jeśli chcielibyśmy wzbudzić zdarzenie dla konkretnego przepływu, podajemy je w parametrze event, np.:
 <g:link controller="user" action="zestawTrzechPytan" event="symbole">
Podejdź do Zestawu Trzech Pytań (seria z symbolami)
</g:link>
W przypadku formularzy, nazwa przycisku (atrybut name elementu g:submitButton) określa zdarzenie.
 <g:form name="formularz" url="[controller: 'uczen', action: 'zestawTrzechPytan']">
Wybierz rodzaj pytań:
<br>
<g:submitButton name="nazwy" value="Nazwy pierwiastków" />
<g:submitButton name="symbole" value="Symbole" />
</g:form>
Wykonanie walidacji formularza wymaga użycia akcji przejścia (ang. transition action), która jest wykonywana przy przejściu z jednego stanu do drugiego. Jeśli wykonanie akcji przejścia zakończy się niepowodzeniem, przejście jest wstrzymywane i przywracany jest ostatni stan, np.:
 odpowiedzNaPytanie1 {
on("submit") {
flow.pytanie1 = new Pytanie(params)
flow.pytanie1.validate() ? success() : error()
}.to "przejdzDoPytania2"
}
Podczas zatwierdzenia formularza pojawia się zdarzenie submit. Domknięcie, które jest opcjonalnym, drugim parametrem wejściowym dla metody on, jest właśnie akcją przejścia.

Skoro wspominamy o walidacji, to możnaby zaangażować do tego klasy poleceń w Grails, które można przekazać jako parametr wejściowy domknięcia dla metody on, np. (Listing 9-42):
 enterCardDetails {
on('next') {CreditCardCommand cmd ->
flow.creditCard = cmd
cmd.validate() ? success() : error()
}.to 'showConfirmation'
}
Grails automatycznie wypełni klasę polecenia danymi użytkownika (z żądania) i jedynie, co należy wykonać, to wywołać metodę validate() (która jest dostarczana również automatycznie przez Grails w każdej klasie polecenia).

Istnieje również możliwość ponownego wykorzystania pewnego bloku kodu (domknięcia), który będzie wykonywany w ramach akcji przejścia jako zmiennej, której wartością jest...domknięcie, np.:
 odpowiedzNaPytanie1 {
on("submit", sprawdzPytanie1).to "przejdzDoPytania2"
}

private sprawdzPytanie1 = {
flow.pytanie1 = new Pytanie(params)
flow.pytanie1.validate() ? success() : error()
}
Możliwe jest warunkowe wskazanie stanu wynikowego (co nie powinno dziwić, kiedy zauważymy, że jest to po prostu parametr wejściowy do metody to, która przyjmuje albo identyfikator stanu, albo domknięcie, które ów identyfikator wylicza), np.:
 odpowiedzNaPytanie1 {
on("submit").to { flow.pewienParametr ? 'stanX' : 'stanY' }
}
Takie wyznaczenie stanu wynikowego nazywamy przejściem dynamicznym, które jest domknięciem będącym z kolei parametrem wejściowym metody to.

Grails Web Flow umożliwia uruchomienie podprzepływów, które są niczym innym jak kolejnym przepływem w ramach już istniejącego przepływu. W ramach stanu S1 wykonujemy metodę subflow, której parametrem wejściowym jest nazwa przepływu. Stany końcowe przepływu wewnętrznego są zdarzeniami, które można przechwycić w przepływie macierzystym.

Na koniec pojawia się 3 stronicowy listing kodu źródłowego całego przykładowego buyFlow. Ciekawa lektura, która robi swoje z i tak zapewne wycieńczonym czytelnikiem. Rozdział 9. kończy się przedstawieniem sposobu, w jaki testuje się przepływy. W tym celu korzystamy z klasy grails.test.WebFlowTestCase. Tworzymy test integracyjny, którego klasa rozszerza WebFlowTestCase z pojedynczą metodą getFlow() zwracającą testowany przepływ jako domknięcie, np. (Listing 9-50):
 class StoreBuyFlowTests extends WebFlowTestCase {
def controller = new StoreController()
def getFlow() {
controller.buyFlow
}
}
Wykonanie metody startFlow() w metodzie testowej spowoduje uruchomienie przepływu. Metody assertFlowExecutionEnded() oraz assertFlowExecutionOutcomeEquals pozwalają na stwierdzenie, czy przepływ się zakończył, i jaki był ostatni stan. Poza nimi mamy assertFlowExecutionActive(), assertCurrentStateEquals(String), signalEvent(String) oraz setCurrentState(String). Z ciekawymi przykładami końcówka rozdziału działa niezwykle kojąco.

Więcej informacji o Grails Web Flow znajdziemy w dokumentacji Grails Webflows.

Kolejny rozdział dotyczy Grails ORM (GORM).

Okazuje się, że nie tylko ja byłem zainteresowany Grails Web Flow. W trakcie pisania tej relacji, znalazł się również pewien zainteresowany mały pajączek. Wie ktoś, coś więcej na jego temat? Pewnie to jego pierwsza w życiu książka i od razu zaczął od Grails ;-)


Nawet udało mi się złapać falę na zdjęciu. Choć trochę oddaje "flow" rozdziału ;-)