Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future: use mysqli or PDO instead in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: mysql_connect(): Headers and client library minor version mismatch. Headers:50562 Library:100020 in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: Cannot modify header information - headers already sent by (output started at /home/www/novacisko.cz/subdomains/bredy/init.php:11) in /home/www/novacisko.cz/subdomains/bredy/index.php on line 38
Distributor - vaše objekty budou o svém okolí vědět vše - Bredyho blog - Ondřej Novák
Bredyho blog - Ondřej Novák

Distributor - vaše objekty budou o svém okolí vědět vše

Největším problémem většiny UI /ale i ne-IU/ aplikací, které potkávám, je špatná nebo vůbec nevyřešená komunikace mezi důležitými objekty. Přitom stačí málo, obstarat si distributora.


Co je distributor

V zásadě nejde o nic převratného. Distributor je rozesílač zpráv. Jeho úkolem je rozeslat zprávu, kterou nějakým způsobem obdrží, všem zaregistrovaným objektům. Jedná se vlastně o doručovací službu, nebo, chcete-li, konferenci. Nic víc, nic míň.

O tom, že to není tak jednoduché se přesvědčíme v následujícím článku, a pokusíme se nastínit nějaké řešení. A aby to bylo kompletní, součástí článku bude i fungující zdrojový kód distributora

K čemu je distributor?

Když pátráme po důvodech, proč vlastně potřebujeme distributor a proč si myslím, že patří mezi důležité objekty aplikačního návrhu, je třeba se dotknout základů komunikace mezi objekty.

Každý OOP programátor umí dávat objektům různé příkazy prostým voláním metod. Odpovědí je vždy návratová hodnota. Tenhle základní postup nám brzo přestane stačit. U složitějších programů totiž objekty začínají žít. Sem tam získají kousek procesorového času, aby provedly nějakou činnost. Například objekty spouštěné pomocí nějakého časovače nebo plánovače. Zvláštní kapitolou pak jsou objekty vlastnící celé vlákno, jež dlouhodobě pracují a chtějí o své činnosti poskytovat nějaké informace. A v konečném důsledku zde máme objekty, které reagují na nějaké události, událost pak zpracují, a podle typu ji přepošlou dál.

Objekty mohou dělat leccos a je tedy potřeba, aby o své činnosti informovaly okolí, ať již synchronně nebo asynchroně. Nestačí nám tedy jednoduchý model: dotaz - v podobě volání metody a odpověď - v podobě návratové hodnoty.

Při návrhu vhodného objektového modelu budeme velice často řešit následující problémy:
Mějme objekt, který má informovat okolí o své činnosti.

  1. Komu to oznámí (kterou metodu kterého objektu zavolá?)
  2. Bude příjemce jeden nebo jich bude víc.
  3. I když teď je příjemce jeden, může se nám hodit, aby v budoucnu existovalo příjemců víc?
  4. Kdo zajistí správu příjemců? Má se o to postarat zdrojový objekt, nebo se to bude řešit na straně příjemce?

Nejde ani tak o otázky implementace, jako spíš o logiku návrhu. Uvedu jednoduchý příklad takové úvahy:

Mějme objekt, který něco vykonává, nějaký výpočet nebo monitoring a o své činnosti informuje prostřednictvím rozhraní nějaký objekt, jehož referenci obdržel při svém vzniku.

Pak zde máme nějaké uživatelské rozhraní, které provádí zobrazování výsledků. Pokud by zde byl vztah 1:1, není co řešit. Při vzniku výkoného objektu zaregistrujeme uživatelské rozhraní přímo, takže výkonný objekt bude toto rozhraní informovat o svém stavu.

Následně příjde požadavek, že je potřeba stav výkonného objektu sledovat z vícero míst, prostředníctvím různých UI. Nastává tedy otázka. Kdo zajistí duplikování zpráv? Kdo ponese zodpovědnost za příjemce?

  1. Mohl by to provádět výkonný objekt
  2. Mohlo by to provádět UI.
  3. Mohl by to provádět někdo třetí.

Ad 1. To není štastné řešení, protože by jsme nutili výkonnému objektu funkcionalitu, která mu nepřísluší.
Ad 2. Také se nejedná o šťastné řešení. Varianta jednoho hlavního UI a dalších podřízených možná bude fungovat dobře, dokud někdo neukončí hlavní UI.
Ad 3. Je jediná správná varianta. A přesně objekt Distributor je ten třetí, který ponese zodpovědnost za tuto činnost.

Představa obecného distrubutora

Distributor<Ifc> dist;
dist.Add(&view1);
dist.Add(&view2);
dist.Add(&view3);
dist.Add(&view4);

dist.AktualizujPohledy(10,aaa,"xxx"); //zprava k rozeslani

Výše uvedený zdrojový kód představuje použití ideálního distributora. Po konstrukci a registraci příjemcu zavoláme metodu, která se zavolá na každé view a "překlopí 1:1" dodané parametry.

Tohle vypadá pěkně, ale jednoduše to implementovat nepujde. Je zřejmé, že by to znamenalo, že distributor musí implementovat všechny funkce rozhraní Ifc tak, že pro všechny registrované objektu zavolá metodu, která se jmenuje stejně. Je to zejména velmi pracné, představme si, že máme rozhraní, kde je 200 metod.

void Distributor<Ifc>::AktualizujPohledy(int i,float a, const char *s) 
{
for(Prijemci::iterator iter = prijemci.begin(); iter != prijemci.end(); iter++)
iter->AktualizujPohledy(i,a,s);
}

výše uvedene ještě 200x.

a to se nezmiňuji o problémech, které může tato jednoduchá implementace přinést

Tuply a zprávy

Trošku reálněji vypadá použití objektu "zpráva" - Message. Jak se v článku dozvídáme, objekt Message poslouží k zabalení volání včetně parametrů. Přesně to co potřebujeme. Stačí, aby někdo vytvořil objekt Message a předal ho distributorovi a ten se postará, aby objekt byl předán všem příjemcům.

Ono to je i logické, prostě rozesílat lze jen zprávy.

Vytvoření zprávy není až tak složité:

Message<NavratHodn>::Make(&funkce, tupleParametry);

Pro zadání parametrů musíme použít tuple.

Problém 1: Jednotnost rozhraní

Ideální distributor měl jednu výraznou výhodu. Dal se použít namísto původního přímeho spojení s UI, bez zásadní změny. Funkce Distributora byly totožné s funkcemi rozhraní, takže ani výkoný objekt, aby UI nemuselo nijak poznat, že zde je někdo mezi. Prostě taková transparentní proxy.

Použitím zpráv jsme přišli o jednotné rozhraní. Je zřejmé, že budeme muset výkonný objekt naučít místo volání metod přímo, zasílat zprávy. A nebo, rozšířením objektu distributora a dopsáním všech metod, stejně jako nahoře. Ale tentokrát to budeme mít snažší. Postačí jen vytvořit zprávu:

void MujDistributor::AktualizujPohledy(int i,float a, const char *s) {
Broadcast(Message<void>::Make(&Ifc::AktualizujPohledy,(tuple|i,a,s));
}

Téhle otrocké práci se prostě moc nevyhneme, tak si ji aspoň co nejvíc zjednoduššíme.

Problém 2: Reentrance

O čem to bude? Už jsem se toho dotknul, týká se vlastního rozesílání. Distributor píšeme spíše na synchronní než asynchronní rozesílání. Synchronní prostředí je v C++ přírozenější a převést synchronní prostředí na asynchronní je jednodušší, než naopak.

Do hry tedy vstupuje cosi jako pořadí volání a reentrance. Představme si, že rozesíláme nějakou zprávu a v rámci obsluhy této zprávy jeden z příjemců rozhodne, že cosi ve výkonném objektu změní. Tato změna způsobí, že výkonný objekt podá o změně zprávu, která se opět musí rozeslat.

Jenže předchozí rozeslání ještě nebylo dokončeno. Co teď?

Metoda 'mrtvého brouka'
je založena na metodě "neřešit to". Prostě se do rozesílací smyčky vstoupí rekurzivně znovu a rozešle se. Tohle má jeden zásadní problém. Představme si, že se rozesílá událost A, a v polovině rozesílání nechá v rámci obsluhy této činnosti rozeslat událost B. Zatímco příjemci vyřízení předtím obdrží události v pořadí A,B, příjemci následující obdrží události v pořadí B,A. Za písmeno A si představte událost Vytvoř "X" a za písmeno B událost Vymaž "X". A problém je na světě
Metoda 'to se nesmí'
Tuto metodu uvádím proto, že jsem ji dlouho používal jako řešení tohoto problému. V zásadě je to jednoduché řešení. Jakmile distributor zjistí, že je volán rekuzivně, vyhodí výjmku nebo assert. Prohlásíme, že v rámci příjmu zprávy se nesmí provádět žádné činnosti vedoucí k novému rozesílání. Ten kdo tohle potřebuje udělat, musí činnost vyvolat asynchroně (vlákno)
Metoda 'odloženo'
Patří mezi chytřejší postupy, kdy rozeslání si pouze zaznamenáme a to se provede až po dokončení současného rozesílání. Zdá se to celkem jednoduché, chytré a dostačující. Avšak i tady nalezneme problém. Ten se objeví v okamžiku, kdy necháváme rozeslat nějaký velký objekt, jenž máme připravený v zásobníku. Ten rozesíláme tak, že posíláme pouze ukazatel. Jenže to se v tomto případě nedá. Musíme rozeslat kopii. Dále nahlédneme, že rozesílání se odkládá jen někdy. To znamená, že ve valné většine případů se kopie udělá zbytečně a úplně by postačilo rozeslat ukazatel. Nehledě na to, že při vytváření kopie budeme řešit problem související s ničením kopie, tj. kdo to udělá a kdy.
Metoda 'zavolejte mi později'
Je variantou, kdy ten příjemce, který potřebuje provést akci vedoucí k rozesílání se dočasně zaregistruje k distributorovi aby byl zavolán nakonec v době, kdy už všichni byly obeslání a nehrozí tedy změnu v pořadí. Otázkou je, co v případě, že takových příjemců je víc, nehledě na tom, že příjemce musí mít k dispozici rozhraní distributora
Metoda 'nezavěšujte, jste v pořadí'
Poslední a zatím nejrozumnější metodu jsem si nechal nakonec. Je založena na myšlence, že příjemce, jenž spustil akci vedoucí k novému rozesílání musí počkat na dokončení současného rozesílání. To v synchronním prostředí nedělá snadno, ale jde to. Jakmile distributor přijme nový požadavek na rozeslání, v rámci stejné úrovně zásobníku dokončí předchozí rozpracované rozesílání. A to bez ohledu na to, kolikrát došlo k rekurzi. Existuje zde fronta naplánovaných rozesílání tak, aby, bez ohledu na aktuální zanoření se vždy rozesílalo v pořadí. I tahle metoda má jeden drobný problém. Pokud budou příjemci na zprávy reagovat akcí vedoucí k rozesílání často, může rychle dojít zásobník. Naopak obrovskou výhodou je, že každý požadavek vedoucí k rozesílání zachovává všechny ukazatele ukazující do zásobníku platné.

Správa příjemců

Ke správě příjemců není toho na psaní. Jistě si každý dovede představit metody Add a Remove, které přidávají nebo mažou příjemce.

Trochu zajímavější to začne být v okamžiku, kdy příjemce požádá o zrušení registrace v rámci zpracování přijaté zprávy. A později zjistíme, že je to velmi častá programátorská technika. Je potřeba, aby současné rozesílání se o této činnosti dozvědělo, a nějak ji reflektovalo, tj, pokud je někde například iterátor procházející příjemce, musí se tento iterátor po zrušení registrace aktualizovat tak, aby ukazoval na stejného příjemce jako před zrušením registrace a nebo aby při dalším cyklu pokračoval přijemcem ve stejném pořadí jako před tím a nikdoho nepřeskočil ani nikoho nevolal dvakrát.

Přitom nesmíme pominout i situace, kdy přijemce v rámci obsluhy přijaté zprávy požádá a zrušení registrace jinému příjemci, který už mohl být obeslán, nebo teprve bude.

Balím to, končím

Tato kapitola by spíš měla být v horní části, kde se píše o tom, proč distributor. Začnete-li používat distributora, brzo přijdete na to, že vám nějak přestává chybět neexistující GC v C++ a nebo že chytré ukazatelé nejsou nějak využité. To v okamžiku, kdy do rozesílaných zpráv přidáte zprávu oznamující všem příjemců, je distributor končí.

Třeba proto, že výkonný objekt skončil svou práci a buď nechal tuto zprávu rozeslat, nebo nechal distributora zničit (nezapomenou distributora podědit a implementaci do destruktoru dopsat).

V okamžiku, kdy distributor končí svou činnost, mohou příjemci na to reagovat například tím, že se také zničí. A nebo se mohou jen deaktivovat. Výhodou toho, že přijemci byli registrovaní na objektu, který končí je, že nemůže nastat situace, že by příjemce zůstal zapomenut v paměti a stal se z něho memory leak.

Tenhle důsledek má i opačný směr. Nemůže nastat situace, že by příjemce pracoval s objektem, který už neexistuje. Kolik takových UI aplikaci je, kde smažete nebo ukončíte činnost nějaké části programu a navazující ovládací prvky nejen že nezmizí, ale buď nebudou reagovat, nebo ohlasí chybu, nebo jejich použití rovnou pošle program do věčných lovišť.

Document - View

Asi nejčastější použití distributora bude v modelu Document - View. Přitom pišu o striktnějším model, který vychází z mých návrhů pro Bohemia Interactive Studio (snad časem napíšu víc). Základem je to, že jak dokument tak view spolu komunikují přes společné (chcete-li oboustranné) rozhraní.

View přes rozhraní posílá dokumentu příkazy a dokument přes toto rozhraní rozesílá všem stejné příkazy obsahující tytéž parametry, nebo nebo modifikované parametry tak, aby odpovídaly skutečně provedené akci (samozřejmě, reakcí může být i jiná zpráva, pokud lépe odpovídá provedené činnosti).

Hlavním smyslem tohoto pravidla je to, že View (UI) odděluje zvlášť ovládací část a zvlášť zobrazující část (i když navenek může fungovat dohromady). View pouze zašle požadavek, ale aktualizaci svého zobrazení provede až v okamžiku, kdy se požadavek vrátí od distributora. To umožňuje View kooperovat s ostatními View, Jakákoliv změna v dokumentu se pak ihned projeví ve všech view naráz a bez zbytečného programování navíc.

Nemusím snad ani ukazovat, že s použitím distributora lze toto implementovat velice snadno. Jediný co bude třeba udělat je řešit "Problem 1", tj všechny metody rozhraní prostě ve zděděném distributoru implementovat přes Message::Make

distributor.h První verze. Může se stát, že není finální. Ke překladu potřebuje tuples a actions
Distributor.cpp Příklad na použití distributora.

dodatek: k článku snad časem přibudou i nějaké nákresy

vytvořeno: 17.8.2007 18:13:01, změněno: 17.8.2007 18:13:01
Jsou informace v článku pro Vás užitečné?
  • (0)
  • (0)
  • (1)
  • (0)
  • (0)
Nick nebo OpenID
Vzkaz
 
2.7.2014 14:19:09

4AzMzbwep

Spokojenost, urcite seooojpnkst! Ty sede plochy jsou docela prodysne, takze noha pekne vetra, material taky zatim drzi, i kdyz botky pouzivam i jako takovy univerzalni sportovni obuti (na volis), pouze snad vytka k tomu cernymu potahu na spici nekde jsem s nim zavadil, tak se trochu seskrabl, ale to je pouze vzhledova vada. Takze muzu urcite doporucit

Podobné články

Endy

Endyho osobní stránka

Úvod do obecného OOP 3. díl

Dědičnost

Vracíme z funkce objekty - problémy

Při praktickém používání technik Vracíme z funkce objekty narazíme na nečekané a možná nelogické problémy.

Vracíme z funkce objekty - praktická ukázka

V článku Vracíme z funkce objekty jsem ukazoval, jak probíhá návrat objektu z funkce a jak toto optimalizovat tak, aby se minimalizovalo používání kopírování. Teď si to ukážeme prakticky - použil jsem gcc bez optimalizací

Vracíme z funkce objekty - Skrytí konstruktoru

Skrývání konstruktoru může být důležité v případě, že potřebujeme v objektu použít Pimpl Idiom. Objekty totiž mohou vznikat i z datových typů, které nechceme zveřejňovat
Reklama: