K čemu je nám užitečný komponentový web framework?

Nedávno jsem jedním svým twítem vyvolal menší diskusi ohledně toho, co nám dovoluje komponentový framework oproti tomu, čeho bychom byli schopni dosáhnout s jednoduchým MVC rámcem. Bohužel twitter mi nedává takovou možnost vyjádřit se a tak jsem chtěl důvody a výhody, které vidím v komponentách na frontendu, popsat trošku obšírněji v tomto článku.

Komponentový model na webové vrstvě přináší oproti MVC se "standardním" šablonovým systémem možnost elegantní znovupoužitelnosti částí, které se znovu použít dají. Pro mě jako vyznavače DRY je toto jedna z VELKÝCH výhod. Namítnete, že znovupoužitelnosti se přeci dá dosáhnout i v běžném šablonovacím systému - co třeba JSP tagy nebo Freemarkerová makra? Touhle cestou jsem si prošel a výsledek byl vždycky kostrbatý - obvykle se vám podaří znovupoužít buď vykreslovací šablonu nebo aplikační logiku, ale nikdy ne rozumně obojí.

Vezměte si jen takový jednoduchý stránkovaný seznam - v nekomponentovém MVC se logika v controllerech míchá, na view vrstvě si sice můžete pomocí makra vysdílet šablonu, ale přechody mezi stránkami, třídění a filtrování už řešíte v controlleru a ten je z podstaty věci obtížné sdílet mezi různými stránkami. Neříkám, že to je nemožné, ale rozhodně to od vás bude vyžadovat daleko větší invenci, než se vám podaří shodný kód vysdílet. Čím složitější stránka, tím složitější controller a tím větší pravděpodobnost, že ze znovupoužitelnosti nezbude nic. Tento způsob tvorby webové vrstvy na to není prostě stavěný. Má ale zase jiné výhody - je jednoduchý.

Komponentový systém nám umožňuje stránkovaný seznam vzít a obalit jej do jedné komponenty, která spojuje vykreslovací šablonu (kterou musí být jednoduché přetížit v případě potřeby - tady třeba selhává JSF), logiku, která se stará o třídění a filtrování a od běžného programátora vyžaduje pouze implementaci rozhraní, které komponenta používá pro získávání dat z aplikační vrstvy.

Chceme-li jít ještě dál můžeme (ale nemusíme) i tuto komponentu dále rozdělit na samostatnou subkomponentu pro stránkování, komponenty pro definici sloupců seznamu (s derivací sloupce, podle kterého je možné třídit), filtrovací komponenty atd. Ve standardizovaných případech tedy pracujeme se stránkovanými seznamy na vyšší úrovni abstrakce a při chytrém návrhu máme velké možnosti kompozice komponent do výsledného požadovaného tvaru a tudíž i velké úspěšnosti znovupoužitelnosti.

Ať jen neteoretizujeme na abstraktní úrovni, tady jsou ukázky z našeho DSL s jehož pomocí skládáme webovou vrstvu:


<recordListing>
   <dataSource class="com.fg.SomeProvider"/>
</recordListing>

Toto to nejjednoduší možné zapsání seznamu - používáme komponentu recordListing (výpis záznamů) s dodavatem dat SomeProvider. Ve vykresovací šabloně nejsme při tvorbě HTML ničím svázáni, ale zase toho moc nevíme k tomu abychom dokázali navrhnout vykreslovací šablonu obecně. Tenhle způsob se ale výborně hodí na specifické layouty seznamů, u kterých víme, že je jejich znovupoužitenost minimální (např. toto je ukázka specifického stránkovaného výpisu).

Co když budeme chtít komponentu ještě dále zobecnit a používat ji jako kopyto pro desítky použití (např. pro účely reportingu)? Dekomponujeme si ji dále na sloupce a můžeme ven vyjmout stránkovací logiku například takto:


<recordListing>
   <dataSource class="com.fg.SomeProvider"/>
   <column id="firstName"/>
   <orderableColumn id="lastName"/>
   <column id="age"/>
   <pagination/>
</recordListing>

Pokud bychom do toho všeho chtěli umožnit ještě filtraci podle obsahu sloupců, můžeme jít ještě dál:


<recordListing>
   <dataSource class="com.fg.SomeProvider"/>
   <column id="firstName"/>
   <orderableColumn id="lastName">
      <columnFilter>
         <textInput id="filter.byName"/>
      </columnFilter>
   </orderableColumn>
   <column id="age">
      <columnFilter>
         <textInput id="filter.fromAge">
            <validators><number/></validators>
         </textInput>
         <textInput id="filter.toAge">
            <validators><number/></validators>
         </textInput>
      </columnFilter>
   </column>
   <pagination/>
</recordListing>

Nechci zabíhat do přílišných podrobností, které jsou poplatné tomu komponentovému frameworku, který jsme použili - šlo mi jen o to naznačit možnosti kompozice webové vstvy.

Znovupoužitelnost je ale jenom jedna z věcí, kterou jsme tím získali. Dekompozice na logicky uzavřené celky nám otevřela cestu k technice částečného vykreslování (obnovování) stránky pomocí AJAXu.

Tím že v aplikaci máme strukturovaný model webové vrstvy můžeme začít využívat reflexního přístupu k řešení některých problémů. Jak bylo vidět na příkladu výše, můžeme se například v recordListing komponentě prohledat subkomponenty na výskyt columnFilter a orderableColumn komponent a podle jejich aktuálních uživatelských dat odpovídajícím způsobem upravit výsledný dotaz, který jde na backend (tj. do poskytovatele dat SomeProvider, který vůbec netuší co všechno se před jeho zavoláním stalo). A můžeme tak udělat i velmi obecně, aniž bychom dopředu museli znát všechna místa a způsoby použití.

Obdobným způsobem jsme třeba realizovali export do XLS na našem firemním intranetu - akce, která generuje přes Apache POI XLS soubor potřebuje v základu minimální množství konfigurace a přesto je dostatečně obecná. Vyhledá si totiž na aktuální stránce komponentu, která zobrazuje stránkovaný seznam, vyčte si z ní konfiguraci sloupců, datové typy a formátování, zjistí si její datový zdroj a vygeneruje Excel ve stejné struktuře s tím rozdílem, že nad datovým zdrojem projde a vypíše všechny dostupné stránky výpisu a nikoliv pouze tu aktuální, jak to dělá webový frontend. Výhodou tohoto provedení je navíc to, že zůstane zachované i uživatelské filtrování a třídění obsahu, jako na webu. Bez reflexe bychom museli odvést daleko větší množství práce, abychom se k rozumně zformátovanému výstupu do Excelu dostali.

Nedávno, když jsme přemýšleli nad stavbou modulárního GUI pro Edeeho nás napadlo, že bychom mohli pomocí AOP techniky elegantně řešit publikace nových gadgetů skrz redakční systém. Představte si situaci, kdy máte nasazený redakční systém s celou řadou specifických vstupních editorů stránek. Podobně jako WordPress má dva typy obsahu: článek a stránku - i my umožňujeme obsah kategorizovat - nicméně nejsme omezeni pouze na tyto dva typy. Pro každého zákazníka šijeme požadované typy obsahu na míru - pokud si zákazník přeje mít jako obsah kuchařský recept (s přísadami, obtížností přípravy atd. atd.), obyčejný článek nebo popis akciového fondu, připravíme mu odpovídající a z hlediska UX optimalizovaný editor.

Na druhé straně připravujeme modul, který umožňuje extrakci statistických údajů z Google Analytics a chtěli bychom, aby se u každého editoru stránky zobrazilo malé udělátko, který by uživateli zobrazilo, jak je na tom jeho stránka s počtem přístupů v čase (pěkně pohodlně rovnou v rozhraní našeho produktu).

Otázka nyní zní: jak zpropagovat náš gadget do existujících editorů stránek? Nesmíme zapomenout, že my při tvorbě udělátka ani nevíme, jaké specifické editory na různých zákaznických instalacích vzniknou.

Nasnadě je tupá varianta - udělátko zdokumentovat a na každé instalaci, kde budeme chtít mít modul Google Analytik zapnutý budou muset programátoři obejít všechny editory stránek a ručně do nich podle dokumentace udělátko přidat. Samozřejmě, když by náhodou někdo modul vypnul, budeme mít na všech těchto stránkách zobrazenou chybu.

Ovšem díky tomu, že v systému máme pod kontrolou prototypovou stránku tohoto vstupního editoru, na které se staví dál, a zároveň máme webovou vrstvu strukturovanou do komponent, můžeme celý problém vyřešit daleko elegantněji.

Napíšeme pravidlo (aspekt), které bude aplikováno na každou stránku spravovanou webovým frameworkem a v případě, že bude daná stránka odvozená z našeho prototypového editoru (pointcut), najdeme na ní předpřipravené místo pro tato extra udělátka a tam náš Google Analytics gadgetek přidáme (advice). Kromě jednoduchého přidání komponenty do nějakého kontejneru máme celou škálu dalších operací, které je možné se skladbou komponent dělat. V tomto ohledu kopírujeme většinu možností manipulace, které nám s DOMem umožňuje jQuery (append, prepend, wrap, remove atd.).

Ani nemusím zmiňovat, že udělátko si s sebou může nést plno funkcionality - přepínat různé typy dat (pageviews, unikátní návštěvníci, délka období atp.), filtrovat atp. Díky zapouzdřenosti komponent není nutné dvakrát koumat, jak vypadá cílová stránka. Vše co komponenta potřebuje, si nese s sebou. To je dar komponentového modelu.

Původně jsme mysleli, že budeme tuto aspektovou rozšiřitelnost používat jen ve velmi výjimečných případech, ale v současné době pozoruji, že tento přístup si získal oblibu a začal se používat na hromadné změny ve stránkách na klientských instalacích poměrně běžně.

Samozřejmě AOP má svá rizika - při extenzivním používání znepřehledňuje vazby v systému (u aspektově přidávaných a modifikovaných věcí nemusí být patrné odkud a proč byla tato modifikace provedena). Pokud se bude AOP na view vrstvě používat opatrně a s rozumem otevírá prostor pro elegantní řešení jinak velmi pracných úprav. Ostatně, když srovnáme toto použití aspektů s AspectJ, který se na byznys vrstvě používá už léta, zjistíme, že pozitivní i negativní dopady na systém jsou obdobné.

A abych dokončil ještě svou myšlenku z Twitteru - zkuste si něco podobného s push MVC frameworkem. Tam toto realizovat IMHO nelze, protože model na view vrstvě chybí (modelem zde není myšlen doménový model). Teď už snad ten můj tweet dává logiku :)