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 {Obowiązkowy identyfikator przepływu (ang. flow id) to nazwa akcji bez kończącego "Flow" (w naszym przypadku będzie to odpowiedz).
def zestawTrzechPytanFlow = {
...
}
}
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 {Pierwszy stan to stan początkowy przepływu.
def zestawTrzechPytanFlow = {
pytanie1 {
...
}
}
}
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 {Taki typ programowania, gdzie tworzy się łańcuch wywołań metod na podstawie wyniku wykonania poprzedniej metody, ma nawet swoją nazwę - "fluent API"!
def zestawTrzechPytanFlow = {
pytanie1 {
on("poprawnaOdpowiedz").to "pytanie2"
on("niepoprawnaOdpowiedz"). to "pomoc"
}
}
}
Stan końcowy to stan, który nie przyjmuje argumentów, albo przekierowuje (ang. redirect) do innej akcji bądź przepływu.
class UczenController {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).
def zestawTrzechPytanFlow = {
pytanie1 {
on("poprawnaOdpowiedz").to "pytanie2"
on("niepoprawnaOdpowiedz"). to "pomoc"
}
ocena()
wpisanieOcenyDoDziennika {
redirect(controller: "dziennik")
}
}
}
W ramach dowolnej akcji stanu możemy wyświetlić inną niż domyślną stronę z pomocą znanej już nam metody render, np.:
def zestawTrzechPytanFlow = {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.
pytanie1 {
render(view: "pytanie")
on("poprawnaOdpowiedz").to "pytanie2"
on("niepoprawnaOdpowiedz"). to "podpowiedz"
}
...
}
class UczenController {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.
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"
}
}
}
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 = {Tak na prawdę to wynik akcji określa zdarzenie, więc w konstrukcjach if-else będziemy korzystać z return, np.:
rodzajPytan {
action {
params.symbolePierwiastkow ? tak() : nie()
}
on("tak").to "seriaPytanZSymboli"
on("nie").to "seriaPytanZNazw"
}
...
}
action {Podobno, od Groovy 1.6, można zapisać powyższy przykład bez return - po prostu same nazwy zdarzeń w postaci bezparametrowych metod.
if (params.symbolePierwiastkow)
return tak()
else
return nie()
}
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">W ten sposób rozpoczynamy przepływ. Jeśli chcielibyśmy wzbudzić zdarzenie dla konkretnego przepływu, podajemy je w parametrze event, np.:
Podejdź do Zestawu Trzech Pytań
</g:link>
<g:link controller="user" action="zestawTrzechPytan" event="symbole">W przypadku formularzy, nazwa przycisku (atrybut name elementu g:submitButton) określa zdarzenie.
Podejdź do Zestawu Trzech Pytań (seria z symbolami)
</g:link>
<g:form name="formularz" url="[controller: 'uczen', action: 'zestawTrzechPytan']">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.:
Wybierz rodzaj pytań:
<br>
<g:submitButton name="nazwy" value="Nazwy pierwiastków" />
<g:submitButton name="symbole" value="Symbole" />
</g:form>
odpowiedzNaPytanie1 {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.
on("submit") {
flow.pytanie1 = new Pytanie(params)
flow.pytanie1.validate() ? success() : error()
}.to "przejdzDoPytania2"
}
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 {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).
on('next') {CreditCardCommand cmd ->
flow.creditCard = cmd
cmd.validate() ? success() : error()
}.to 'showConfirmation'
}
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 {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.:
on("submit", sprawdzPytanie1).to "przejdzDoPytania2"
}
private sprawdzPytanie1 = {
flow.pytanie1 = new Pytanie(params)
flow.pytanie1.validate() ? success() : error()
}
odpowiedzNaPytanie1 {Takie wyznaczenie stanu wynikowego nazywamy przejściem dynamicznym, które jest domknięciem będącym z kolei parametrem wejściowym metody to.
on("submit").to { flow.pewienParametr ? 'stanX' : 'stanY' }
}
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 {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.
def controller = new StoreController()
def getFlow() {
controller.buyFlow
}
}
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 ;-)
Brak komentarzy:
Prześlij komentarz