Hlavní navigace

Seaside (7)

18. 4. 2005
Doba čtení: 7 minut

Sdílet

Návrhový vzor Observer (Pozorovatel) patří k těm nejčastěji používaným. Dnes se podíváme na to, jakým způsobem se ve Smalltalku implementuje a kdy a jak ho využívat ve webových aplikacích napsaných pomocí Seaside.

Běžně bývá webová aplikace chápána jako množina datově oddělených stránek, které uživateli prezentují nějaká data. Ty se generují často jednorázově z databáze. U Seaside však bývá vzájemná provázanost dat a prezentačních komponent často velmi komplexní. Proto je nezřídka potřeba využít mechanismus závislostí mezi daty a jejich prezentací.

Observer

Smalltalk návrhový vzor Pozorovatel používá doslova na každém kroku. Jeho nejčastějším úkolem je vybudovat vazbu mezi modelem a pohledem na model, občas však také slouží k provázání různých modelů.

Základní výchozí situace je taková, že máme model reprezentující nějaký objekt s daty a k němu předem neznámý počet závislých objektů, které potřebují s modelem pracovat a případně reagovat na jeho změny. K implementaci Observeru ve Smalltalku se používají dva standardní přístupy, z nichž každý se hodí v jiných případech.

Model

Každý model musí nějakým způsobem referencovat své závislé objekty (pozorovatele). Nejintuitivnější je využít k tomu instanční proměnnou, která bude obsahovat jejich kolekci.

Object subclass: #Model
    instanceVariableNames: 'dependents'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Kernel-Objects' 

Ve Squeaku se nejčastěji používá pole třídy DependentsArray, které je pro tento účel optimalizovano.

Vazba mezi modelem a pozorovatelem se pak vytvoří na základě zaslání zprávy addDependent: a zruší pomocí zprávy removeDependent: Při tom se neprovede prakticky nic jiného, než že se objekt přidá do kolekce pozorovatelů či z ní odebere.

Vytvořme si nyní jednoduchý demonstrační příklad, který bude obsahovat tři třídy. Jednu třídu pro model zapouzdřující data, dále pohled, který bude reagovat na změnu stavu modelu, a nakonec editor, který bude schopen stav modelu měnit. Jak pohled, tak editor si budou udržovat referenci na model, aby z něj mohli čerpat data.

Model subclass: #MyModel
    instanceVariableNames: 'data'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Dependency'

Object subclass: #MyView
    instanceVariableNames: 'model'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Dependency'

Object subclass: #MyEditor
    instanceVariableNames: 'model'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Dependency' 

Model bude obsahovat pouze přístupové metody. V případě, že se změní obsah modelu, informuje o tom své závislé objekty pomocí metody changed, kterou zašle sám sobě.

MyModel >> data
    ^ data.

MyModel >> data: anObject
    data := anObject.
    self changed. 

Metoda changed je zjednodušenou verzí metody changed:, která projde kolekci závislých objektů a pošle jim zprávu update: s jedním parametrem. Ten je v případě použití pouhého changed přímo referencí na model.

O provázání s modelem se postará přímo pohled sám v okamžiku, kdy mu model specifikujeme.

MyView >> model: aModel
    model := aModel.
    model addDependent: self. 

Pohled tak může při změně modelu například informovat uživatele.

MyView >> update: aParameter
    self inform: ('my model {1} changed' format: model asString). 

Editor se rovněž bude sám starat o provázanost s modelem. Pro MyView a MyModel by bylo vhodné definovat společného předka, který bude společnou metodu model: implementovat. Na tom, jak se bude reference na model ve skutečnosti jmenovat, samozřejmě nezáleží a mohlo by se jednat (a často také jedná) o nějaké méně abstraktní pojmenování, ze kterého je patrná totožnost modelu (např. osoba, firma apod.). Náš editor bude provádět úpravu modelu v metodě upravModel

MyView >> model: aModel
    model := aModel.
    model addDependent: self.

MyView >> upravModel
    model data: 42 

Použití by pak mohlo vypadat takto:

| model view1 view2 editor |
model := MyModel new.
view1 := MyView new model: model.
view2:= MyView new model: model.
editor := MyEditor new model: model.
editor upravModel. 

Editor by mohl přirozeně na změny modelu také nějakým způsobem reagovat.

Zmínili jsme metody changed a changed: Ve skutečnosti může být propagace změn ještě o něco komplikovanější. Její schéma je ve Squeaku následující:

Model >> changed
    self changed: self

--> Model >> changed: aParameter (model)
        self dependents do: [:aDependent | aDependent update: aParameter]

Dependent >> update: aParameter 

respektive (pro více parametrů)

Model >> changed: anAspect with: anObject self
    dependents do: [:aDependent | aDependent update: anAspect with: anObject] -->

Dependent >> update: anAspect with: anObject
    ^ self update: anAspect

Dependent >> update: aParameter 

Je dobré mít přehled o tom, jak propagace změn pracuje, abyste věděli, jak (hlavně update metody) správně přetěžovat a nezarazili propagaci v případě, že na změnu mohl pohled ještě reagovat na abstraktnější úrovni. Tedy dávat pozor, kdy, jak a zda vůbec necháváme propadnout změnu v metodě update:with: Také je dobré si uvědomit, že metody changed a changed: nejsou nijak provázány s metodou update:with:, takže je chybou očekávat, že se v ní o změnách dozvíme, byť s nedefinovanými parametry.

Aspekt slouží k uvedení bližších informací o tom, o jakou změnu se jedná. Často se předává ve formě symbolu.

Model >> pridejOsobu: osoba

    self osoby add: osoba.
    self changed: #pridejOsobu with: osoba.


Model >> odstranOsobu: osoba

    self osoby remove: osoba.
    self changed: #odstranOsobu with: osoba.


SeznamOsob >> update: aspect with: object

    aspect = #pridejOsobu
        ifTrue: [ self pridejNahledPro: object ].

    aspect = #odstranOsobu
        ifTrue: [ self odstranNahledyOsobPro: object ]. 

DependentsFields

Metody changed:, update: apod. jsou implementovány standardně přímo ve třídě Object. Nicméně když se podíváte na definici této třídy, neobsahuje žádnou instanční proměnnou se seznamem závislých objektů. Bylo by také velmi nerozumné, kdyby každý objekt včetně čísel a jiných triviálních objektů obsahoval své vlastní pole závislých objektů.

Pro instance tříd odvozených od objektu se využívá jednotná kolekce nazvaná DependetsFields, které je třídní proměnnou třídy Object. Pokud objekt dostane např. zprávu changed a snaží se zjistit svoje závislé objekty, podívá se do této instance třídy IdentityDicti­onary. Mezi klíči najde sám sebe. Nalezená hodnota je pole závislých objektů, které k němu náleží. Pokud mezi klíči není, vrátí se prázdné pole. Pole závislých objektů tedy ve výsledku mají pouze ty objekty, které je skutečně potřebují, za což se platí dražším přístupem.

Objekty, u nichž je jasné, že budou závislosti využívat často, se odvozují od třídy Model, která má pole závislých objektů jako instanční proměnnou. Z vnějšku se ale jak subinstance Objectu, tak Modelu tváří samozřejmě stejně.

Seaside a Observer

V souvislosti s komponentním modelem se vynořuje jeden problém, který je potřeba řešit právě pomocí mechanismu závislostí.

Představme si, že máme model, který obsahuje kolekci seznamu osob. Pro tento model si vytvoříme komponentu, která bude seznam zobrazovat jako textový výpis nebo třeba pomocí metody rendereru list:

renderContentOn: html

    html list: self model osoby 

Vazba komponenty na model může být jednostranná. Model nemusí nijak komponentu referencovat.

Pokud do kolekce osob v modelu přidáme další a uživateli se vyrenderuje stránka s komponentou, bude seznam vykreslen správně.

Naprosto jiná situace ale nastane v okamžiku, kdy seznam osob reprezentujeme nikoliv jako jednu monolitickou komponentu, ale jako komponentu, která bude obsahovat kolekci samostatných dceřiných komponent s informacemi o jednotlivých osobách.

renderContentOn: html

    nahledyOsob do: [ :nahled | html render: nahled ] 

Když vytvoříme komponentu se seznamem osob, vytvoříme dceřiné komponenty s podrobnějšími informacemi a vše necháme vyrenderovat. Problém nastane v okamžiku, kdy do modelu přidáme další osobu. Protože dceřiné komponenty jsme vytvořili jen při inicializaci komponenty se seznamem, a ta stále existuje (dojde pouze k jejímu opětovnému vykreslení), změna se v seznamu nijak neprojeví. To samozřejmě může nastat i v okamžiku, kdy jsme osobu přidali třeba ze stránky, která nemá s tou se seznamem na první pohled nic společného.

Může se jednat například o situaci, kdy máme zobrazenu stránku se seznamem, pomocí volání komponent se dostaneme do jiného stavu aplikace, kde nějakým způsobem přidáme další osobu, a pak se opět přes různé okliky dostaneme zpět. Komponenta se seznamem celou tu dobu existovala jako živá instance a při opětovném zobrazení se znovu neinicializovala. V důsledku tedy seznam dceřiných komponent nebude odpovídat skutečnosti.

Proto je nutné v případech, kdy komponenty obsahují jiné komponenty a jejich zobrazený obsah není jednorázově generován, zavést závislosti na model, kdy např. u kolekcí budete vytvářet nebo popřípadě rušit dceřiné komponenty podle aktuálních změn modelu.

U složitějších aplikací s robustní objektovou strukturou je důležité mít tento fakt dobře na paměti. Seaside je neskutečně mocný nástroj. O to zákeřnější ale při neuváženém zacházení dokáže být.

Trvanlivost závislostí

Provazování modelu a komponent má jeden nepříjemný důsledek. Smalltalk pracuje s garbage collectorem, díky němuž jsou objekty svázány se sezením uživatele a ruší se v okamžiku, kdy již nejsou potřeba, tedy např. při vypršení maximální doby neaktivity sezení.

Pokud ale propojíme přes mechanismus závislostí model a komponenty, dojde k tomu, že uživateli zobrazené komponenty budou modelem stále referencovány, a nebudou tedy nikdy zrušeny! Na model se budou nalepovat nové a nové komponenty, takže krom enormní paměťové zátěže budou propagované změny modelu trvat velmi dlouho.

U komponent se doba jejich platnosti definuje těžko a samy o své likvidaci mohou rozhodovat jen omezeně, takže o jednoduchém použití zprávy removeDependent:, a tedy uvolnění vazby mezi komponentou a modelem si můžeme nechat jen zdát.

Naštěstí řešení existuje a je velmi jednoduché a elegantní. Stačí totiž, aby kolekce závislých objektů modelu byla definována jako WeakArray

bitcoin_skoleni

MyModel >> initialize

    super initialize.
    dependents := WeakArray new. 

Weak kolekce obsahují reference, které garbage collector nebere v potaz, takže takto referencované objekty s chutí zlikviduje v okamžiku, kdy již nikým jiným odkazovány nejsou.

Jediná potíž nastává v okamžiku, kdy chceme využít závislosti i na definování vazeb v rámci domény modelu. Pak by se mohlo stát, že o některá data přijdeme. V takovém případě nezbude nic jiného než pole závislostí rozdělit na weak a perzistentní část.

Seriál: Seaside

Autor článku