Java EE framework sa v posledných rokoch vo verzii Java EE 6 a Java EE 7 zjednodušil natoľko, že je možné vytvárať Java EE aplikácie s jednoduchou štruktúrou, ktoré obsahujú veľký podiel aplikačnej logiky a malý podiel spojovacieho kódu. Na vystavanie aplikácie nám často stačia obyčajné Java objekty obohatené o anotácie, ktoré definujú často sa opakujúce vlastnosti a mechanizmy. Metódy objektov tak obsahujú hlavne business logiku, teda funkcionalitu našej aplikácie.
Všeobecné mechanizmy, ako napríklad logovanie, transakcie, napojenie na databázu či vytváranie vzťahov medzi objektami, je možné deklarovať pomocou dodatočných anotácií. Tie slúžia ako značky pre Java EE framework, ktorý podľa nich vloží dodatočnú funkcionalitu do našich objektov. V tomto prípade ide o funkcie, ktoré sa často opakujú a dajú sa všeobecne automatizovať.
Ukážeme si ako môžeme prehľadne štruktúrovať našu aplikáciu, a ako nám pri ohraničení stavebných blokov pomáhajú Java EE anotácie, ktoré dávajú logický význam jednotlivým objektom.
Rozdelenie na prezentačnú a business logiku
Typická Java EE webová aplikácia sa rozdeľuje na prezentačnú vrstvu (frontend) a vrstvu s business logikou (backend). Toto rozdelenie je logické vzhľadom k tomu, že prezentačná vrstva býva sama o sebe dosť obsiahla a sú na ňu obvykle kladené odlišné nároky ako na hlavnú časť aplikácie. Ak oddelíme business logiku od frontendu, môžeme na ňu pripájať viac rôznych prezentačných komponent alebo ju sprístupniť externým aplikáciám napr. ako webovú službu. A zaručíme tak, že všetky prezentačné rozhrania budú používať tie isté business pravidlá a chovať sa rovnako.
V našej ukážkovej aplikácii začneme tým, že zobrazíme na úvodnej stránke oslovenie pre prihláseného užívateľa.
Vytvoríme 2 obyčajné Java triedy:
- IndexPage bude obsahovať prezentačnú logiku – metódu na vrátenie textu, ktorý zobrazíme na stránke (vrátane pozdravu a mena užívateľa)
- UserContext bude obsahovať business logiku – spôsob, akým sa zistí aktuálne prihlásený užívateľ – a poskytuje navonok jeho meno cez public metódu
[restabs alignment=”osc-tabs-left” responsive=”true” icon=”true” text=”More”]
[restab title=”index.xhtml” active=”active”]
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"> <h:head> <title>Welcome to the CRM</title> </h:head> <h:body> <h1>#{indexPage.welcomeMessage}</h1> </h:body> </html>
[/restab]
[restab title=”IndexPage.java”]
package eu.ondrom.crmapp.presentation.boundary; import eu.ondrom.crmapp.business.boundary.UserContext; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.inject.Named; @Named @RequestScoped public class IndexPage { @Inject private UserContext userContext; public String getWelcomeMessage() { return "Welcome " + userContext.getCurrentUserName() + " !"; } }
[/restab]
[restab title=”UserContext.java”]
package eu.ondrom.crmapp.business.boundary; import javax.enterprise.context.Dependent; import javax.transaction.Transactional; @Transactional @Dependent public class UserContext { public String getCurrentUserName() { return "Lisa"; } }
[/restab][/restabs]
Každá trieda je definovaná v balíčku, ktorý nesie v názve to, do akej vrstvy patrí (IndexPage v balíku „presentation“, UserContext v balíku „business“). Obe vytvorené triedy sa ešte nachádzajú vo vnorených balíkoch „boundary“ – obe ohraničujú logiku svojej vrstvy a poskytujú rozhranie navonok:
- IndexPage oddeľuje logiku prezentačnej vrstvy od definície užívateľského vzhľadu, ktorý je definovaný pomocou xhtml súboru JSF faceletu
- UserContext oddeľuje business logiku od logiky prezentačnej vrstvy
V balíkoch „boundary“ budeme umiestňovať iba hraničné triedy. Ostatné triedy budeme dávať do balíka control, ktorý nebude zaujímavý mimo svojej vrstvy. Ale o tom neskôr.
IndexPage.java a UserContext.java sú veľmi podobné v tom, že sú to obyčajné Java triedy. Odlišujú ich použité anotácie:
- @Named je jedno z najpoužívanejších označení pre objekty dostupné v JSF stránkach, preto prirodzene označuje IndexPage ako hraničný objekt medzi JSF stránkou a našou prezentačnou logikou.
- @Transactional zapína na objektoch transakčné chovanie voči externým zdrojom, ktoré ho podporujú (hlavne voči databázi) a zabezpečuje tak atomicitu vykonávanej operácie. Takéto chovanie je veľmi často očakávané pre business operácie a zaručuje, že buď operácia prebehne celá úspešne, alebo sa všetko vráti do stavu pred jej začiatkom. Anotácia @Transactional je dostupná iba v Java EE 7. Ak aplikačný server podporuje iba staršiu Java EE 6, je potrebné použiť anotácie, ktoré označujú nejaký typ EJB (napr. @Stateless).
Ďalšie anotácie definujú, ako budú jednotlivé inštancie Java tried vytvárané a prepojené:
- Označenie prepojení na závislosti:
- @Inject označuje závislosť objektu na inom objekte. V triede IndexPage je takto označená objektová premenná (field) userContext, do ktorej sa má vložiť objekt typu UserContext.
- Použitie premennej v JSF označuje závislosť na pomenovanom objekte v prezentačnej logike. V index.xhtml je to premenná indexPage, ktorá očakáva objekty tried pomenovaných ako indexPage.
- Označenie samotných závislostí, aby boli korektne vytvárané a napájané na ostatné objekty:
- @Named dáva textový názov danej triede – JSF podľa neho vyhľadá objekt, ktorý vloží do premennej s rovnakým názvom. Takto je pomenovaná trieda IndexPage – jej meno je odvodené z názvu triedy (začína však malým písmenom). Názov môžeme explicitne uviesť priamo v anotácii @Named.
- @RequestScoped, @Dependent a podobné anotácie označujú, že objekty z danej triedy je možné použiť ako závislosť pre iné objekty. Tieto anotácie zároveň označujú scope – rozsah dĺžky života jednej inštancie. Objekt typu IndexPage, ktorý je označený ako @RequestScoped, bude vytváraný vždy jeden pre odbavenie celého požiadavku (request). Objekt typu UserContext, ktorý je označený ako @Dependent, bude vyváraný vždy znova pri každom vložení do iného objektu alebo premennej.
V našom jednoduchom prípade nie je dôležité, aké rozsahy (scope) sú zvolené, pretože zatiaľ sa všetky objekty chovajú konštantne – oba vrátia pri volaní metódy tú istú hodnotu. Nie je teda rozdiel, či je volaný objekt vždy jeden, alebo voláme viackrát iný objekt. Odlišnosti medzi rôznymi scope preberieme neskôr, keď budú mať vplyv na logiku aplikácie a budeme potrebovať medzi nimi rozlišovať.
Štruktúrovanie logických blokov
Či už ide o prezentačnú logiku, alebo business logiku, kvôli prehľadnosti je vhodné oddeľovať jej kód do blokov podľa domény, ktorej sa týkajú. Doména obsahuje logiku a zdrojové súbory, ktoré spolu úzko súvisia – napr. doména pre prihláseného užívateľa obsahuje kód prihlasovacieho formulára, prihlásenia užívateľa a zobrazenie prihláseného užívateľa na stránkach. Na backend logike potom existuje doména pre overenie prihlasovacích údajov, načítanie dát o užívateľovi z databáze a zistenie, ktorý užívateľ je práve prihlásený. Zdrojové súbory každej domény obyčajne dávame do oddelených Java balíčkov (adresárov).
Samotné domény obsahujú jednu alebo viac logických častí:
- boundary – súčasti domény, ktoré sú používané z iných domén alebo vrstiev aplikácie. Obsahujú hraničné objekty, interface, anotácie – všetky aktívne prvky a ich pomocné súčasti, ktoré chceme sprístupniť navonok
- model – dátový model domény, ktorý obsahuje zvyčajne uchovávaný (perzistentný) stav objektov v doméne. Najčastejšie sú to JPA entity, ktoré sú ukladané a načítané z databáze. Ale môžu to byť aj iné typy objektov, ktoré je možné nejak externalizovať a uložiť/načítať, v prípade prezentačnej vrstvy je to model pre grafické komponenty.
- control – ostatné časti domény, ktoré nezapadajú ani do boundary ani do model. Nie sú súčasťou dátového modelu. ani sa „nezmestia“ do hraničných objektov v boundary. Sem umiestňujeme vnútornú logiku domény, ktorá súvisí s dátovými konverziami, znovu‑použiteľnými výpočtami, alebo napríklad aj s reportami, či konektormi na externé systémy.
Doména by určite mala mať časť boundary, ktorá definuje použitie domény mimo tejto domény. Vo väčšine prípadov budeme potrebovať aj dátový model, a teda časť model. Ak sa doména rozrastie, ostatná logika bude v časti control. Toto rozdelenie je v súlade so ECB vzorom, v ktorom sa namiesto termínu model používa aj termín entity, ako to definoval Ivar Jacobson. ECB štruktúru domén odporúča aj Adam Bien na jeho blogu a knihách o Java EE vzoroch.
Mohlo by sa zdať, že 3 časti na postihnutie všetkých prípadov nestačia, a že sa časť control musí vždy skôr či neskôr neúmerne rozrásť. Väčšina aplikačnej logiky je však často tak jednoduchá, že dokonca pre ňu stačia iba časti boundary a model. A to hlavne vtedy, keď je treba iba dáta z databáze preniesť do prezentačnej vrstvy a zobraziť v aplikácii. Všetka potrebná logika je tak zvyčajne obsiahnutá v jednoduchých metódach objektov v boundary, prípadne je použitá funkcionalita z iných domén.
V menej častých prípadoch, keď sa logika rozrastá, by sme mali zvážiť, že doménu rozdelíme na menšie domény, ktoré môžeme už bez problémov deliť na boundary, model a control. Ak to z rôznych dôvodov nedáva zmysel, môžeme ďalej štruktúrovať časť control tak, že jej kód ďalej členíme do podbalíčkov, ako napr. control.converters, control.persistence, control.processing, control.reports, control.configuration. Takýmto spôsobom je stále dobre čitateľné, kde sú hranice domény a kde je dátový model. Samotná vnútorná logika komponenty, ideálne „neviditeľná“ mimo domény, je oddelená, ale pod drobnohľadom stále dobre organizovaná.
Výsledná štruktúra aplikácie
Ak uplatníme popísané postupy, je štruktúra aplikácie čitateľná pri letmom pohľade na zdrojový kód a ľahko sa v nej orientuje. Dve odlišné logiky frontendu a backendu sú oddelené, čo nám umožňuje izolovať od seba dva veľké celky, a prípadne napojiť na backend iné prezentačné rozhranie. Pre niektoré typy projektov a týmy, ktoré na nich pracujú, je takto možné aj rozdeliť úlohy tak, aby sa na frontend a backend logike dalo pracovať súčasne bez veľkých kolízií medzi členmi týmu.
Hranice jednotlivých logických vrstiev môžeme jednoducho definovať pomocou Java EE anotácií. Hranica medzi JSF komponentami a logikou je napr. definovaná anotáciou @Named, hranica medzi frontend a backend logikou je definovaná hlavne transakčnými anotáciami, ako sú @Transctional, alebo EJB anotácie (napr. @Stateless).
Nasledujúci diagram zachycuje tradičnú 2-vrstvovú architektúru (respektíve 3-vrstvovú, kde treťou vrstvou je databáza napojená na entity manager či data-access objekty):
Ako aplikácia narastá, oba veľké bloky frontendu a backendu môžeme ďalej štruktúrovať do menších domén, podľa funkcionality, ktorej sa logika týka. Tieto domény majú vždy obsahujú časť boundary, ostatné časti model a control podľa potreby a veľkosti domény. Takto môžeme postupovať aj pre menšie komponenty v doménach. Pre typickú aplikáciu na správu zákazníkov tak môžeme backend rozdeliť napríklad na domény Zákazníci, Zmluvy, Projekty, Kontakty, ktoré budú ďalej členené podľa vzoru boundary, control, model. Môžeme tieto domény ešte ďalej rozčleniť na komponenty, napríklad doménu Zákazníci ďalej na komponenty Správa zákazníkov a Komunikácia, v ktorých uplatníme rovnakú schému na členenie kódu.
Referencie
- kniha od Ivara Jacobsona: Object Oriented Software Engineering: A Use Case Driven Approach
- video blog Adama Biena: Bureaucratic Design With Java EE
- projekt Lightfish, ktorý používa rozčlenenie podľa vzoru ECB (miesto termínu model je použitý termín entity)