Dekorátory jsou jakési odlehčené komponenty, které obalují ty skutečné. Jak fungují, si ukážeme na jednoduchém dekorátoru, který bude obaleným komponentám dodávat schopnost skrývat se.
Dekorátory se odvozují od třídy WADecoration. Ten náš bude obsahovat instanční proměnnou uchovávající aktuální stav skrývání.
WADecoration subclass: #HideDecoration
instanceVariableNames: 'hidden'
classVariableNames: ''
poolDictionaries: ''
category: 'DecorationDemo'
Protože se jedná o typický případ, kdy v komponentě/dekorátoru máme proměnnou vázanou na aktuální stav uživatelského rozhraní, zaregistrujeme si ji pro backtracking pomocí třídy WAStateHolder.
initialize
super initialize.
hidden := WAStateHolder new contents: false.
hidden
^ hidden contents
hidden: aBoolean
^ hidden contents: aBoolean
Stejně jako komponenty se i dekorátory vykreslují v instanční metodě renderContentOn:. Svého vlastníka vykreslují pomocí metody renderOwnerOn:. Stav skrývání budeme přepínat pomocí odkazů.
renderContentOn: html
html div: [
html anchorWithAction: [ self hidden: self hidden not ]
text: (self hidden ifTrue: [ 'show' ] ifFalse: [ 'hide' ] ) ].
html div: [
self hidden ifFalse: [ self renderOwnerOn: html ] ].
Komponentě vlastníka se dekorátory přidávají jednoduše pomocí zprávy addDecoration:. Ideální místo bývá v inicializační metodě, nicméně dekorátory lze přidávat či odebírat libovolně za běhu.
initialize
super initialize.
self addDecoration: HideDecoration new.
V tom, že dekorátory mohou být dynamicky aplikovány či odebírány, spočívá jejich největší síla. Každá komponenta může mít na sebe aplikováno zároveň několik dekorátorů. Při aplikaci se dekorátory standardně přidávají na vnější vrstvu, ale toto pořadí lze ovlivňovat.
Dekorátor se ze seznamu obalujících komponent odstraňuje pomocí zprávy removeDecoration:. Oproti běžné komponentě se liší v jednom podstatném rysu. Dekorátory jsou vázané v kontinuaci, a jejich výskyt je tedy separátně definován pro každou stránku. Pokud si tedy například do komponenty přidáte tlačítko, které ji bude obalovat naším skrývajícím dekorátorem, postupně si jich přidáte třeba šest a následně se pomocí zpětné navigace v prohlížeči vrátíte o několik stránek zpět, bude komponenta obalována řekněme dvěma dekorátory. V okamžiku, kdy přidáte další, bude obsahovat správně tři dekorátory a nikoliv sedm.
Delegace
O tom, že dekorátory jsou velmi mocné prostředky, svědčí i fakt, že tak zásadní vlastnosti Seaside, jako jsou například vzájemné volání komponent nebo izolace backtrackingu, jsou řešeny pomocí nich. Delegace přes zprávy call: a answer: je prováděna pomocí dekorátorů WADelegation a WAAnswerHandler.
Při zaslání zprávy call: nějaké komponentě se tato komponenta obalí dekorátorem WADelegation, který velmi jednoduchým způsobem přesměrovává rendering na určenou komponentu. Při návratu se tato dekorace odstraní, takže se na stránce opět objeví původní komponenta. Celý tolik opěvovaný mechanismus call:/answer: je díky tomu ve skutečnosti naprogramován na přibližně čtyřiceti řádcích kódu včetně hlaviček metod.
Pro aplikačního programátora má fakt, že se volání komponent dělá pomocí dekorátorů, jeden důležitý dopad. Musí umět zajistit, aby jeho vlastní dekorátory delegační dekorátory dodané Seaside buď předcházely, nebo jimi naopak byly obaleny.
To se zajišťuje pomocí instanční metody isGlobal, která je standardně nastavena na false, tedy dekorátory jsou umisťovány uvnitř delegátorů. Dekorátory, které vyžadují, aby delegace probíhala v jejich nitru, musí tuto zprávu přetížit tak, aby vracela true. Pokud si komponenty potřebují nějakým dalším způsobem rozšiřovat vzájemné pořadí dekorátorů, které jsou na ně aplikovány, mohou si vhodným způsobem přetížit metodu decoration:shouldWrap:.
Obecné akce
Dekorátory mají velmi často hodně obecný charakter. Proto o konkrétních akcích, které budou vykonávat, nezřídka rozhodují jiné objekty. Vezměme si například situaci, kdy budeme chtít našemu dekorátoru pro skrývání doplnit akci pro smazání modelu asociovaného s dekorovanou komponentou nebo komponenty, která představuje vlastníka dekorátoru, pokud je tato komponenta prvkem kolekce podobných komponent (ač to tak nevypadá, při důkladném přečtení tato věta skutečně dává smysl; pozn. autora) (výzva pro NLP; pozn. Johanka). Nejjednodušší je při kliknutí na odkaz poslat vlastníkovi dekorátoru (owner) adekvátní zprávu.
html div: [
html text: self asOop asString.
html anchorWithAction: [ self hidden: self hidden not ]
text: (self hidden ifTrue: [ 'show' ] ifFalse: [ 'hide' ] ) .
html space.
html anchorWithAction: [ self owner delete ] text: 'delete' ].
Toto provedení má ale jeden koncepční nedostatek. Problém nastane v okamžiku, kdy pracujeme s vlastnickou komponentou, která má obecný charakter a může se tedy v aplikaci objevit vícekrát. Náš dekorátor může například obalovat komponentu EditorOsoby, ale ta jen stěží bude mít konkrétní představu, v jakém kontextu má být modelovaná osoba smazána. Můžeme ji chtít mazat z databáze nebo třeba z nějakého lokálního seznamu. Proto se v těchto situacích raději používá předávání bloků. Blok, který se bude při mazání provádět, budeme definovat přímo v dekorátoru. Ten tak budeme moci používat pro obalování libovolných komponent a nebudeme při tom muset předpokládat, že vlastník rozumí zprávě delete.
WADecoration subclass: #HideDecoration
instanceVariableNames: 'hidden deleteAction'
classVariableNames: ''
poolDictionaries: ''
category: 'DecorationDemo'
deleteAction
^deleteAction
deleteAction: anObject
deleteAction := anObject
Standardně se bloky vyhodnocují pomocí zprávy value. V dekorátoru by tedy vykreslení odkazu pro smazání vypadalo takto.
html anchorWithAction: [ deleteAction value ] text: 'delete' ].
Problém je v tom, že současný Squeak chápe bloky jako jistý druh literálu a
nejedná se o tzv. čisté closure, což se projeví například v následujícím kódu:
editoryOsob do: [:editor |
" decorator := dekorator pro editor "
decorator deleteAction: [editoryOsob remove: editor] ].
Pokud bloky chápeme jako literály, znamená to, že jsme do deleteActionpřiřadili pokaždé stejný blok. Tomuto bloku jsme v cyklu do: pouze několikrát předefinovali kontext. Výsledkem tedy bude, že lokální proměnná editor bude v každém editoru osoby referencovat poslední prvek kolekce editoryOsob!
Teoreticky by sice stačilo, abychom do deleteAction přiřazovali vždy kopii bloku, tedy
editoryOsob do: [:editor |
" decorator := dekorator pro editor "
decorator deleteAction: [editoryOsob remove: editor] copy].
ale tato konstrukce bohužel není z implementačních důvodů přípustná.
Řešení existují dvě. První je použít speciální upravený druh kompilátoru, který si s touto konstrukcí již poradí. Druhou prozatím standardnější metodou je použít vhodným způsobem parametry bloku.
editoryOsob do: [:editor |
" decorator := dekorator pro editor "
decorator deleteAction: [:e | editoryOsob remove: e] ].
Skutečnou instanci editoru, tedy bloku, dodáme jako parametr až při jeho vyhodnocení.
html anchorWithAction: [ deleteAction value: self owner ] text: 'delete' ].
Podobné konstrukce určitě nepatří k těm nejprůhlednějším, ale je potřeba o jejich existenci alespoň tušit.
Autentizace
Seaside standardně implementuje několik tříd dekorátorů. Již jsme se v jednom z předchozích dílů zmínili o validačním dekorátoru. K těm dalším zajímavějším patří například WAFormDecoration, který obaluje vlastníka formulářem s tlačítky, nebo WASessionProtector, který omezuje přístup ke svému obsahu pouze na jednu klientskou IP adresu.
Velmi užitečný dekorátor je WABasicAuthentication. Ten umí chránit svůj obsah uživatelským jménem a heslem. Návštěvníkovi stránek ukáže standardní přihlašovací dialog.
Nejjednoduššeji se používá prostřednictvím komponentní metody jménem authenticateWith:during:, která jako první parametr přijímá autentizátor a jako druhý blok, během něhož má být autentizace provedena. Autentizátor je objekt, který dokáže reagovat na zprávu verifyPassword:forUser:.
Pokud chceme chránit celou naši aplikaci, je na místě to udělat na vyšší úrovni prostřednictvím upraveného renderovacího cyklu. Vytvoříme si jeho vlastní třídu
WARenderLoopMain subclass: #SecureRenderLoop
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'DecorationDemo'
V něm předefinujeme metodu createRoot tak, aby kořen obalila autentizačním dekorátorem. Autentizátor bude renderovací cyklus sám.
createRoot
| root |
root := super createRoot.
root addDecoration: (WABasicAuthentication new authenticator: self).
^ root
Nakonec zbývá definovat samotnou verifikační metodu pro ověření jména a hesla. Oba tyto údaje jsou dodány jako řetězce
verifyPassword: password forUser: username
^ (username = 'root') & (password = 'toor').
Pro to, aby nám autentizace začala fungovat, ještě musíme v konfiguraci naší aplikace přetížit položku Main Class na náš SecureRenderLoop.
Pro aplikace, které vyžadují autorizaci pouze pro jednoho uživatele, už je připravena třída WAAuthMain, jež si validní uživatelské jméno a heslo bere z konfigurace aplikace, kterou je potřeba rozšířit o WAAuthConfiguration. Podrobněji se ale práci s konfiguracemi budeme věnovat až příště.