Nutnost použití vzoru OBSERVER pro zamezení nepříjemných efektů zpětných funkcionálních vazeb mezi objekty
autor RNDr. Ilja Kraval, http://www.objects.cz únor 2007 firma Object Consulting s.r.o.
© Ilja Kraval, 2007, http://www.objects.cz
Úvod V předešlému článku s názvem Jedna z velmi častých a závažných chyb při návrhu IS aneb jak vznikají tzv. "molochální systémy", (článek je volně ke stažení na serveru www.objects.cz v sekci Odborné články zdarma) se „vřele nedoporučuje“ vytvářet tzv. „molochální systémy“, které svou povahou jdou přesně proti principům komponentní technologie: Na rozdíl od systému, který je logicky navržen po vrstvách a který je následně je rozčleněn do odpovídajících fyzických komponent, tak „molochální systém“ není fyzicky rozčleněný systém. Obsahuje fyzicky „velká kusiska zdrojového kódu ke kompilaci“, nad kterými pracuje několik, někdy i několik desítek vývojářů. Systém „moloch“ není rozdělen fyzicky na menší části, které by se vzájemně linkovaly (uses, include, import apod.). Protože „vše souvisí se vším“, tak systém apriori nejde fyzicky rozdělit. Práce s takovým systémem je velmi nepříjemná. Kompilace je nejenom technicky, ale hlavně organizačně náročná. Vývojáři se nad kódem doslova bijí a dohody, které musí při zásahu do kódu neustále činit, silně stěžují práci. Kód je sám o sobě netransparentní, nečitelný, se spoustou neobvyklých a nečekaných vazeb. Každá i sebemenší změna nutně vede k zásahu do již hotového celého kódu jako obrovského celku, což přináší další problémy. Systém navržený jako moloch jde evidentně svou povahou přesně proti způsobu tvorby IS pomocí moderních komponentních technologií (Java, DOT NET)… Musím poznamenat, že tato situace je nepříjemná nejenom pro vývojáře, ale i pro vedoucího pracovníka. Efektivní řízení vývojových prací nad molochem je v podstatě nemožné anebo opravdu velmi obtížné s náběhem na žaludeční vředy. Jedno z doporučení, jak se vyhnout návrhu systému s takovými vlastnostmi, spočívá v tvorbě velmi dobrého a kvalitního analytického neboli konceptuálního modelu s přísnou kontrolou „čistoty pojmů“. O tom detailněji pojednává zmíněný minulý článek. Ukazuje se však, že toto pravidlo je sice nutné, avšak není dostačující. Můžeme vytvořit z hlediska čistoty pojmů opravdu dokonalý konceptuální model, můžeme tento konceptuální logický model dobře rozvrstvit a následně se jej pokusit fyzicky rozčlenit do relativně nepříliš velkých komponent v dané technologii…a přesto zjistíme, že nastanou efekty příznačné pro kódovaného molocha! V čem je tedy problém?
strana 2
© Ilja Kraval, 2007, http://www.objects.cz
Zpětné funkcionální vazby mezi objekty Odpověď spočívá v efektu chybně vyřešených zpětných funkcionálních vazeb. Vysvětlíme si tento jev na jednoduché situaci: Zpětná funkcionální vazba pro auta a barvy Představme si, že některý objekt používá nějaký jiný objekt, má k němu přístup a přitom tento druhý objekt první objekt nepoužívá, tj. vazba je evidentně jedním směrem. Například se jedná o následující situaci v logickém modelu: Každé evidované auto má barvu z číselníku barev, což zapíšeme pomocí jazyka UML v modelu tříd takto:
Auto
+moje barva
Barva 1
obrázek 1 Zápis části logického modelu "auto má barvu z číselníku barev" Směrovost vazby se samozřejmě promítne až do kódu a to v libovolném prostředí jako jsou unity v Pascalu, knihovny v C++, assembly v C# …atd. Evidentně všechna část kódu, která spravuje pravou část, tedy kód, který spravuje číselník barev, může stát samostatně (například v jedné nebo několika komponentách), levá část kódu (vše, co souvisí s auty), si bude „linkovat“ tuto pravou část. Můžeme si pomyslně představit „střih“ systému, který rozdělí systém logicky a následně i fyzicky na dvě části takto:
strana 3
© Ilja Kraval, 2007, http://www.objects.cz
Auto
+moje barva
Barva 1
obrázek 2 Představa o střihu systému v logice Přitom je zřejmé, že „levá část“ používá „pravou část“. Všimněme si důležité věci: Pravá část kódu (číselník barev) může stát samostatně, nabízí své použití, tj. můžeme ji vyvinout jako první se vším všudy, tj. bez levé části (aut), kterou vyvineme až následně. Vidíme evidentně vrstvy kódu jako vrstva vnitřní a vrstva vnější (barvy a auta), to nám může pomoci vyhnout se molochálnímu přístupu… Připomeňme jenom závěr z předešlého článku, a totiž ten, že musíme dodržet čistotu pojmů a do části kódu spravující barvy (vpravo od střihu) nesmíme „zaplantat“ nic z aut, tedy jinak řečeno barvy musí zůstat barvami „čistými“, což má svou jasnou logiku. Vše se tedy zdá být jasné a logické, ale při pokusu o střih nás čeká jedno nemilé překvapení: Co se stane, když se něco změní v evidenci barev a přitom ta evidovaná auta, kterých se tato změna týká, na tuto změnu musí zareagovat? Jednoduchý příklad: Potřebujeme vymazat z evidence nějakou barvu a všechna auta, která si na ni ukazují, si mají ukazovat od té chvíle na „nic“. Je třeba podotknout, že daná situace se nemusí vyskytnout pouze u tak jednoduchého případu, jako je reakce nějakého prvku na vymazání. Úplně stejná situace nastává například při použití asociační třídy, kdy prvek z asociační třídy musí zareagovat na změnu u prvku, jehož instanci používá (tj. na kterou si ukazuje), anebo se může jednat o logický problém v business logice, kdy někdo potřebuje zareagovat na nějakou změnu u toho, koho „vidí“ (například reakce na změnu stavu v účtu, nebo reakce na novou historii u prvku, apod.). Při řešení těchto problémů se můžeme dopustit vážné chyby: Při běhu nějaké funkcionality v barvách (ať už v kódu střední vrstvy anebo v databázi), kdy dojde ke změně stavu v barvách a auta na tuto změnu stavu mají zareagovat, tak povinně nesmí funkcionalita barev přímo zavolat funkcionalitu aut. To je totiž chybný postup narušující předešlou logiku střihu a vnitřních a vnějších vrstev! Tímto postupem, kdy voláme funkcionálně „zpět“, dojde ke zpětné funkcionální vazbě proti směrovosti použití a vazba mezi auty a barvami se tak stane cirkulární. Velmi nemilou záludností na této situaci je to, že v modelu tříd (který je statický), není tato skutečnost zpětné funkcionální vazby na první pohled vůbec vidět…
strana 4
© Ilja Kraval, 2007, http://www.objects.cz
Závěr je jasný: Pokud funkcionalita barev zavolá funkcionalitu aut, potom se nám již nepodaří oddělit tyto dvě vrstvy od sebe, protože jsou oboustranně cirkulárně provázány (barvy volají a tedy potřebují auta) a pokud tento postup „zpětné funkcionální vazby“ provedeme pokaždé a všude, kde potřebujeme vyřešit podobný problém, máme zaděláno na molochální systém se všemi důsledky. Otázkou tedy je, jak tuto situaci vyřešit správně. Ale než přistoupíme k odpovědi, zmíníme se ještě o jednom velmi důležitém důsledku tohoto chybného postupu, který souvisí s metodickými postupy ve vývoji.
Změna starého kódu při přidání nového kódu? Jedna z doporučovaných metodik vývoje středních, velkých případně rozsáhlých informačních systémů se nazývá iterativní a inkrementální metoda. Velmi stručně řečeno, tato metodika doporučuje u jen trochu větších systémů rozdělit systém na menší části, vyřešit v jedné iteraci vývoje vždy jednu část a k ní poté přidat další části řešení (inkrementace) a opět vyvinout tuto část v další iteraci, přidat další část řešení…atd. Asi nemusíme zdůrazňovat, že pokud dbáme na vrstvy systému, je tato metodika mnohem schůdnější. Dovedeme si představit, že v předešlém příkladu bychom nejprve v první iteraci vyvinuli vše, co se týká barev a poté vše, co se týká aut. Vrstvy nám svou povahou nabízejí návod, odkud postupovat, kde začít, co na co navazuje. Na druhou stranu, pokud nedbáme na vrstvy v systému (pochopitelně s důsledkem tvorby molocha), iterativní a inkrementální metoda nebude fungovat dobře anebo dokonce nebude fungovat vůbec. Pro nás je nyní zajímavá tato situace: Představme si, že k již hotovému kódu potřebujeme přidat nový kód. Může se jednat o jednu z těchto tří situací: •
změnové řízení dané verze systému („na něco se pozapomnělo“),
•
vývoj nové verze („nová verze umí ještě něco navíc“)
•
nebo se jedná o novou iteraci v iterativní a inkrementální metodě vývoje („je třeba přidat a řešit další část systému v další iteraci“)
Ať už se jedná libovolnou z těchto tří situací, tak důležité je zde slůvko přidat kód, tj. máme na mysli situaci, kdy se nám logicky jeví, že se jedná o přidání nové funkcionality beze změny původního již naprogramovaného kódu. Logicky by se nám strana 5
© Ilja Kraval, 2007, http://www.objects.cz
mohlo zdát, že pokud přidáme nový kód s novou funkcionalitou, tak bychom se nemuseli „hrabat“ ve starém kódu. Ano, pokud… Tato logika bezesporu platí, avšak pouze v případě, že jsme dobře vyřešili problém vrstev a také problém zpětné funkcionální vazby. Vraťme se k názornému a jednoduchému příkladu s barvami a auty (viz obrázek 1). Nechť nějaká reakce na změnu v evidenci barev se vyřešila chybně zpětným voláním funkcionality aut. Symbolicky zapíšeme tuto situaci v pseudokódu takto: Nějaká funkcionalita barev FB1; { //...něco zde běží a nastala změna stavu v barvách // je třeba zavolat něco z aut: zavolám nějakou funkcionalitu aut FA1; // pokračuji dále v běhu... } Je zřejmé, že tato konstrukce bude fungovat a dokonce bude fungovat „bezchybně“, ale bohužel konstrukce je špatná, protože část kódu barev volá něco z aut („plantáme“ auta do barev!). Tato čistě teoretická úvaha má dva praktické a velmi nepříznivé důsledky. O prvém z nich už víme: Nelze od sebe striktně oddělit obě dvě vrstvy, auta a barvy, přičemž častým používáním tohoto špatného řešení nám hrozí tvorba molocha. Druhý nepříznivý důsledek si uvědomíme, když přidáme novou entitu (doposud jsme ji neřešili), označme ji jako X, která si také bude podobně jako auta ukazovat do barev:
strana 6
© Ilja Kraval, 2007, http://www.objects.cz
+moje barva
Auto
Barva
1
1 +moje barva
X
obrázek 3 Přibyla další entita X, její prvky používají také barvy Pro náš příklad je důležité, že způsob, jakým entita X přibyla do řešení, je jedna ze tří zmíněných situací: buď se jedná o změnové řízení, nebo o novou verzi anebo o novou iteraci. Jinak řečeno, důležitá je ta skutečnost, že starý kód je již hotov a zkompilován, a my chceme přidat nový kód. A máme zajímavý logický problém! Zdálo by se, že pouze přidáváme novou entitu a proto se nemusíme „hrabat“ ve starém kódu. Avšak pokud se podíváme na konstrukci volání funkcionalit v našem pseudokódu, tak pokud také prvky z X mají zareagovat na tutéž změnu stavu jako auta (což bude velmi pravděpodobné), tak musíme otevřít starý kód barev a přidat jeden řádek s voláním funkcionality X, která (podobně jako u aut) ošetří reakci prvků X: Nějaká funkcionalita barev FB1; { //...něco zde běží a nastala změna stavu v barvách // je třeba zavolat něco z aut a něco z X: zavolám nějakou funkcionalitu aut FA1; zavolám nějakou funkcionalitu X FX1; // pokračuji dále v běhu... } A co když někdo přijde s další entitou Y… a další a další entitou? Vidíme evidentní nepříznivý důsledek číslo 2: Při tomto chybném řešení nastává efekt, že přidání kódu znamená vždy otevření starého kódu a jeho změny…
strana 7
© Ilja Kraval, 2007, http://www.objects.cz
Řešení zpětné funkcionální vazby mezi objekty Existuje několik způsobů, jak vyřešit problém zpětné funkcionální vazby mezi objekty. Všechny jsou postaveny na stejné myšlence, kterou si nyní vysvětlíme. Podívejme se na předešlý kód se dvěma řádky volání funkcionalit a představme si, že by se nám podařilo nějakou fintou zavolat nikoliv jako dva řádky, ale cyklem podle schématu: Cyklus čítač i od 1 do N { F[i] //zavolání i-té funkcionality } (v našem případě F[1]reprezentuje funkcionalitu aut FA1 a F[2]reprezentuje funkcionalitu FX1 a pochopitelně N = 2) V tom případě je problém evidentně vyřešen. Do seznamu funkcionalit můžeme přidat klidně další a další funkcionality a původní kód cyklu se nemění. Jestliže například někdo přijde s další entitou Y, starý kód nebudeme již otvírat (viz cyklus, který se nemění), pouze je třeba novou funkcionalitu přidat do cyklu jako prvek F[3]. Jedinou otázkou zůstává, jak toho efektu docílit. Existuje několik možností, jak vytvořit cyklus z funkcionalit. Volání funkce přes ukazatel na funkce Jedna z možností, kterou nabízí strukturální programování, je vytvořit seznam ukazatelů na funkce a s ním pracovat podle předešlého schématu. Pak skutečně hovoříme o „i-té funkci“. Toto řešení bychom zvolili například v C jazyce (tj. v C bez ++). Volání přes proměnnou typu funkce Některé jazyky nabízejí předešlý způsob práce s ukazateli na funkce ve vyšší syntaxi jazyka přímo deklarací proměnné typu funkce (například Pascal). Vytvořil by se
strana 8
© Ilja Kraval, 2007, http://www.objects.cz
seznam z takovýchto proměnných typu funkce a s nimi by se pracovalo podle předešlého schématu. Volání přes název funkce Některá prostředí neumožňují práci s ukazateli a ani neznají objektové programování ale poskytují možnost zavolat funkci přes její název uschovaný jako hodnota stringu v nějaké proměnné. Nazvěme symbolicky takové volání jako CallFunctionByName(název_funkce: string) Funkce CallFunctionByName převezme hodnotu název funkce jako vstupní parametr a poté zavolá funkci s názvem, který odpovídá hodnotě vstupního parametru. Nyní stačí založit tabulku (obecně seznam) názvů funkcí a poté ji cyklem přečíst a u každé načtené hodnoty zavolat CallFunctionByName. Přidat novou funkci znamená přidat nový řetězec s odpovídajícím názvem do tabulky funkcí. Volání přes delegáty Jazyk C# umožňuje řešení této situace přes tzv. delegáty. Jedná se vlastně o řešení seznamu odkazů na metody objektů. Princip je opět stejný… Volání přes polymorfní metodu – použití vzoru OBSERVER (alias LISTENER) Opravdu objektově čistým řešením je použití vzoru OBSERVER (v jazce JAVA se nazývá LISTENER). Poznámka: Problematiku návrhových vzorů a vzor OBSERVER si můžete podrobně prostudovat v knize „Design patterns v OOP“ zdarma ke stažení na našem serveru. Jak známo, vzor OBSERVER umožňuje zavést mechanismus sledování objektu tak, že když tento objekt změní stav, ostatní objekty na tuto změnu zareagují, avšak nedojde k přímé vazbě od sledovaného objektu k těmto objektům, tj. sledující a reagující objekty jsou zavolány „nepřímo“ přes polymorfní metodu. Jednoduše řečeno, vzor OBSERVER funguje tak, že naše funkcionality nebudeme schovávat ani za ukazatel na funkci jako v C, ani za proměnnou typu funkce, ani za proměnnou typu string s názvem funkce a ani za delegáta, ale „schováme“ ji za polymorfní metodu (podrobně viz uvedená kniha).
strana 9
© Ilja Kraval, 2007, http://www.objects.cz
Závěr Z uvedeného vyplývá, že pokud chceme správně vrstvit systém a zamezit cirkulárním vazbám, musí se nutně použít vzor OBSERVER anebo použijeme nějakou jeho obdobu. V každém případě je nutností odstínit přímé volání sledujícího objektu od sledovaného objektu (tj. zamezit přímému volání proti směru statické vazby). Při zanedbání tohoto pravidla jednak hrozí tvorba nepříjemných molochů (jako přímý důsledek cirkulárních referencí) a kromě toho vzniká neblahý efekt, kdy přidání úplně nového nezávislého kódu nutně vede k otevírání již existujícího starého kódu. To má mimo jiné velmi nepříznivý vliv na postupy při změnovém řízení, při vývoji nové verze a při použití iterativní a inkrementální metody vývoje. Jak vidět, problematika tvorby vrstev v informačním systému je velmi důležitá a není jednoduchá. Musím však z vlastní praxe poznamenat, že v mnoha SW firmách je tento problém bohužel při návrhu IS velmi hrubě opomíjen a to se všemi důsledky. Oproti tomu školení o modelování informačních systémů v UML, která jako autor vedu, se zabývají podrobně jak syntaxí UML, tak se také věnují tzv. „doporučeným a nedoporučeným postupům“. A konkrétně problematice, jak v systému vytvářet necirkulární vrstvy (a následně „dobré“ komponenty), a jak se tedy vyhnout molochům, se věnuje celá jedna velká kapitola i to se spoustou příkladů dobrých a špatných postupů… --- Konec článku ---
strana 10