Zavádění objektově orientovaného programování do již existujícího projektu

 

 

2. (závěrečná) část

 

autor: RNDr. Ilja Kraval, listopad 2010

 

Object Consulting s.r.o.

 

Server objektových technologií

 

http://www.objects.cz

 

 

 

 

 

 

 

Úvod

 

V minulém článku jsme se začali věnovat často se opakujícímu dotazu, jak do již existujícího projektu nasadit objektově orientované programování.

 

Připomeňme si, že se v tomto případě nejednalo o dotaz ohledně překlopení systému z jednoho vývojového prostředí (například FOXPRO) do jiného prostředí (například .NET), ale že v tomto případě projekt pokračuje ve stejném kompatibilním prostředí, pouze se žádá „více používat objekty“.    

 

V minulém článku byla také zdůrazněna ta skutečnost, že objektové programování není samo o sobě samospasitelné, ale je oproti strukturovanému pojetí technologicky velmi výhodné.

 

Byly vyjmenovány hlavní výhody použití objektové technologie:

 

1. Snazší mapování analytických modelů do technologie, dokonce již obstojně podporované „automaty“, například pomocí technologie Hibernate resp. N-Hibernate apod.

 

2. Striktní dodržování zapouzdření v OOP výrazně zvyšuje stabilitu programu a také možnosti testování.

 

3. OOP výrazně napomáhá získat flexibilitu programu pomocí tzv. Design Patterns.

 

Z tohoto pohledu si nyní rozebereme praxí ověřené zásady postupu zavádění OOP do stávajícího existujícího projektu napsaného strukturovaným přístupem.

        

 

Každá závažná změna je potencionální riziko krachu projektu

 

 

Dlouholetá zkušenost vedoucího analytika, případně vedoucího projektu ale i externího konzultanta, mne naučila postoji k navrhovaným změnám, který by se dal nazvat jako „zdravý pesimismus“ (žádné bezhlavé „Yes, We Can!“ J )   

 

Nemám tím na mysli dodržovat zásadu„zuby nehty se bránit každé změně“, ale je třeba dodržovat princip opatrné zdrženlivosti: Při každé myšlence na závažnou změnu v projektu je vždy dobré a přínosné realisticky zvážit všechna pro a proti, ale hlavně si uvědomit, co daná změna znamená z hlediska nutných postupu prací k jejímu dosažení. Mnohdy se totiž nadšení vývojáři soustředí pouze a jenom na výhody stavu „po změně“, a to je velmi nebezpečné, protože k tomuto kýženému stavu je třeba se nějak dobrat a tam se skrývá problém. 

 

Z uvedeného plyne první praktický závěr:

 

Největší chybou pro zavádění jakýchkoliv změn v projektu je přehnaný optimismus a přehnané bezhlavé nadšení pro kýžený výsledný stav, přičemž se však nezohlední nutné postupy prací pro dosažení tohoto kýženého stavu.

 

Mohu navíc z vlastní zkušenosti k této zásadě podotknout, že pro zavádění OOP v již existujícím projektu tato věta platí obzvlášť. Pokud se totiž vývojář seznámí s výhodami OOP a domyslí do důsledku všechny výhody této technologie oproti strukturovanému programování, tak se mu z odborného hlediska přestane pochopitelně stávající strukturovaně pojatý program líbit. To může nakonec vést k sice pravdivé myšlence: „Uznej, jak by to bylo krásné, kdyby to takto bylo“, ale ke špatnému a nebezpečnému závěru: „…a proto to tedy předělejme!“

 

Poznámka: V tomto případě bych doporučoval jako doplňkový argument použít „lavór se studenou vodou a ručník“ J.

 

Je třeba postupovat střízlivě a logicky a nenechat se unést krásou OOP. Vyjmenujeme si proto základní výhody OOP a spolu s tím současně posoudíme jejich relevantnost z hlediska možnosti jejich zavedení ve stávajícím již fungujícím strukturovaně pojatém projektu.

 

Z předešlého výčtu výhod OOP vyplývá, že se v podstatě jedná o tato dvě hlediska změn při přechodu na OOP:

 

1. Zavedení OOP pro změny pro mapování statické struktury programu.

 

2. Zavedení flexibility v programu pomocí OOP.

 

Tato dvě hlediska si nyní prakticky rozebereme.

 

 

zavedení OOP pro Změny pro mapování VE Statické struktuře programu

 

Jedná se vlastně o možnost využít výhod tříd v OOP jako kvalitativně lepších struktur programu, než jakými jsou pouze proměnné a funkce ve strukturovaném programování. Porovnejme si výhody a nevýhody takové změny.

 

Výhody OOP vyplývající ze změn struktur programu:

 

1.     Třídy mají v kódu vyšší transparenci.

 

2.     Je možné použít automatické mapování objektových tříd do databáze (použití Hibernate, N- Hibernate apod.) resp. naopak generování objektových tříd z databáze (Entity Famework apod.) resp. generování tříd jak v OOP, tak tabulek v RDB z CASE nástroje při využití technologie MDA (Model Driven Architecture), resp. využití jiných podobných buď plně automatických anebo poloautomatických postupů.

 

3.     Program díky zapouzdření vykazuje vyšší stabilitu programu a kvalitativně lepší možnosti testování.

 

Postup změn:

 

Již zavedené proměnné bychom museli vyměnit za třídy s atributy a namísto funkcí bychom zavedli metody jako nové struktury programu.

 

Rozbor výhod a nevýhod:

 

Z uvedeného navrženého postupu je patrné, že u již existujícího a napsaného strukturovaného programu je přínos této změny příliš malý v porovnání s tím, jak velký kus práce a s tím spojená rizika by nás s touto změnou čekaly:

 

Celý stávající funkční program by se vlastně navrhoval znovu v nových strukturách - ve třídách. Takovéto „překopání“ stávajícího systému doslova „z gruntu“ by vlastně znamenalo navrhnout ze stávajícího fungujícího a obchodně stále dobře vynášejícího systému celý nový systém s novými strukturami (třídami).

 

Je třeba zdůraznit, že by bylo mylné domnívat se, že by se jednalo pouze o velmi jednoduchý refaktoring (tj. přeskupení) kódu v programu, dokonce že by se dal napsat program jako nějaký pomocný můstek pro toto překlopení. Navrhnout správně třídy a metody objektů není vůbec jednoduché (vychází totiž již z analytického modelu) a změny by znamenaly také i nové tj. další kódování.

 

Poznámka: Takovéto změně z gruntu bychom se pochopitelně nevyhnuli při přechodu z jednoho prostředí do jiného prostředí, které není kompatibilní s předešlým prostředím, například z FOXPRO do C# apod. Celý systém by se musel navrhnout „znovu“, a dokonce pokud nemá z minula z historických důvodů analytický model, tak samozřejmě začínáme již od něj. Zde však uvažujeme o zadání shora ve smyslu: Navazujeme na stávající funkční program v daném prostředí, pouze jej chceme doplnit více o „objektovost“, což je velký rozdíl, než vyměnit prostředí.  

 

Když se navíc podíváme na bod 2 předešlého výčtu, je zřejmé, že výhoda „možnosti rychlého poloautomatického mapování“ v podstatě odpadá, protože náš strukturovaný program má tento krok již za sebou.

  

Závěr:

 

Změnit stávající program a zasahovat do jeho struktur jenom proto, aby dané struktury byly objektové, nestojí za tu práci a hlavně navíc hrozí riziko, že nově překopaná agenda bude „jako čerstvá“ více chybová.

 

Avšak jiná situace se týká nových agend a nových řešení, která navazují na stávající řešení, anebo pokud se přijme změnový požadavek na výraznou změnu stávající agendy. V tom případě se jedná o kvalitativně jinou situaci: U nové agendy resp. při výrazném předělání staré agendy je určitě velkou výhodou zavést postupy spojené s využitím OOP, protože výhody objektové technologie již převáží nad nevýhodami, které vyplývají z mnoha změn, které jsou stejně tak jak tak nutné.

 

Z uvedeného plyne jednoduchý závěr: Co je nové, ať už nová agenda, anebo z gruntu překopaná stará agenda, bude již v technologii navrhováno objektově, ale do starého dobrého strukturovaného programu raději nehrabeme jen proto, aby byl tzv. objektový.

 

    

Zavedení flexibility v programu pomocí OOP

 

 

Zde se jedná již o jiný pohled na využití OOP. Zavedení polymorfismu totiž umožňuje úplně jiný náhled na to, co programátor nazývá „větvením“.

 

Problematikou flexibility získané pomocí polymorfismu se zabývá již klasická oblast OOP zvaná Design Patterns v OOP (tzv. GOF). Vývojáři by měli být s touto problematikou dobře seznámeni a měli by ji hojně využívat, a to dokonce i tehdy, když je program navržen strukturovaně.

 

Základní myšlenka je následující: Pomocí překryté metody (někteří programátoři říkají nepřesně „přepsané“) lze vyměnit v run-time za běhu programu danou funkcionalitu za jinou. Toho se dá využít pro výrazné zvýšení flexibility programu.

 

Uvedená problematika je také vysvětlena v článku 86 na našem serveru v kapitole „Jak funguje flexibilita v OOP a postup jejího vyhledávání“. Předpokládejme, že se v projektu nenacházejí opakující se části kódu popisované v článku 86 jako velmi nešťastné řešení CTRL C  + CTRL V. Pokud se tato šílená metoda použila, potom je problém vážnější a projekt vyžaduje hlubší revizi: Musí se totiž vyhledat opakující se části a kód je třeba zefektivnit pomocí opětovné použitelnosti.

 

Dalším důležitým krokem je třeba zhodnotit přepínače (switch, select case apod.) a zavedené konstrukce IF… ELSE zejména podle těchto hledisek:

 

Typový přepínač

 

Ve strukturovaném kódu se snažíme najít jeden nebo více přepínačů, u kterých k větvení dochází na základě hodnoty reprezentující typ něčeho, (neboli na základě hodnoty tzv. kódu diskriminátoru, pojem diskriminátor viz například kniha Analytické modelování IS pomocí UML v praxi).

 

Pokud tomu tak je, pak bychom měli namísto strukturovaného větvení zavést tzv. „přepínání typovým uměním“, tj. pomocí překryté metody. Tento požadavek se stává nezbytným, pokud se podobný přepínač podle téhož typu znovu vyskytne anebo pokud taková situace reálně hrozí.

 

Jako názorný příklad „ze života“ bych uvedl strukturovaný přepínač pro chování kočky a psa, slovně vyjádřený takto:

 

Funkction Vydání_zvuku (typ zvířete)

 

{

  Select podle typ zvířete:

    Case Kočka: Mnoukej

    Case Pes: Štěkej

  End Select

}

 

Uvedený pseudokód je signálem pro zavedení dědičnosti s abstraktním předkem Zvíře a pro zavedení abstraktní metody zvířete Vydej zvuk, kterou následně překryjeme pro typ kočka a pro typ pes.

 

Pokud k podobnému větvení podle téhož typu (zde zvířete) dojde ještě jinde pro jiné činnosti, anebo to reálně hrozí, potom dokonce musíme tuto konstrukci pomocí přepsání metody určitě a bezpodmínečně zavést. Například kromě větvení pro vydání zvuku u kočky a psa najdeme ještě větvení ve funkci Braň se takto:

 

Funkction Braň_se(typ zvířete)

 

{

  Select podle typ zvířete:

    Case Kočka: Škrábej

    Case Pes: Trhej

  End Select

}

 

Tedy pokud tato opakující se situace switch podle typu zvířete nastala anebo zřetelně může nastat, měli bychom polymorfní chování nasadit určitě a bez výhrad, protože uvedené přepínače začínají být „cestou do pekel“.

 

Díky překrytí metody vzniknou „rozumně geneticky typové dané objekty“, u kterých nepřepínáme chování switchem, ale k přepnutí dojde podle logického pravidla „jak se kdo narodí (jakého typu je), tak se i chová“.

 

Další výhodou je to, že nový podtyp snadno přidáme tak, že podědíme nový podtyp Zvířete a implementujeme zavedené metody. Chování prvku z nového podtypu se automaticky bez dalšího zásahu do kódu projeví ve všech bodech volání.

 

Za další dost silnou výhodu považuji i to, že pokud vrchní metody u nadtypu (zde Zvíře) zvolíme jako abstraktní, kompilátor nás donutí implementovat metody u všech nových dědiců a nepustí nás dále, dokud všechny nevyplníme, tj. nehrozí nám jako u switche, že bychom v některém z nich neimplementovali některou z větví. To je napřklad velkou výhodou při přidání nové abstraktní metody u zvířete.

JA

Vyhledávání flexibilní reakce jiných částí systémů na události

 

Další změny týkající se flexibility při nasazení OOP souvisejí s událostmi v systému.

 

V mnoha případech je třeba v určitém důležitém bodě změny stavu prvku schovat přímá volání funkcí v daném bodě za interface (resp. za delegáta) z toho důvodu, že takovýchto volání bude v daném bodě do budoucna určitě více.

 

Například v určitém bodě programu bankovního systému, kde se ukončilo zaúčtování převodního příkazu, by bylo třeba, aby na tuto změnu stavu zareagovaly funkcionality v jiných částech systému a byly zavolány (úvěry, termínované vklady atd.). Pokud napíšeme v pseudokódu něco v tomto smyslu:

 

// bod zaúčtování

call F1;

call F2;

call F3;

 

tak máme do budoucna problém. Tento bod zaúčtování je totiž natolik důležitý, že se dá očekávat, že při přidání nové části systému resp. přidání nové funkcionality se bude i z ní vyžadovat, aby byla zavolána její dosud netušená reakce.

 

V té chvíli potřebujeme otevřít starý kód, přidat zavolání nové funkce (tj. řádek call F4;) a poté jej opět zavřít. Tento jev je jednak nepříjemný z hlediska údržby (otevírá se zbytečně hotový kód), navíc ještě dochází k cirkulárnímu zpětnému provázání mezi reagujícím kódem a kódem, který jej volá a díky tomu nelze tyto kódy od sebe oddělit do rozumných komponent (o tomto problému viz například vyjmenovaná doporučení v kapitole Modulární nůžky v knize Analytické modelování).

 

Řešením je zavedení vzoru OBSERVER (v Javě zvaný LISTENER) anebo zavedení událostí a delegátů v C#. Uvedené funkcionality se díky tomuto vzoru sice vyvolají, ale jsou přitom schované za interface OBSERVER resp. za mechanismus volání delegátů, čímž se dosáhne kýžené flexibility.

 

Akce a jejich spouštění

 

Jako další podnět k nasazení flexibility pomocí OOP bych uvedl celou problematiku volání akcí, handlování s nimi, výměny akcí, jejich uspořádání, apod. Problematika spouštění akcí je často spojena také s agendou přístupových práv.  

 

Ve strukturovaném přístupu se problém akcí řeší opět pomocí přepínače. Oproti tomu objektově orientované řešení nabízí vzor COMMAND. Zavede se horní abstraktní třída (může být interface) s názvem například Akce a s abstraktní metodou například s názvem Execute. Pro každou ze spustitelných akcí se zavede dědic přepisující tuto metodu. Každou akci pak reprezentuje objekt z nějaké subtřídy Akce a proto jsou akce mezi sebou kompatibilní a dá se s nimi pracovat jako s objektovými proměnnými.

 

Výměna funkcionality v daném bodě zpracování

 

Jako další možnost nasazení OOP si uveďme požadavek na výměnu algoritmu v daném bodě zpracování. Například v daném bodě se provede nějaký přepočet, nebo se prvek „zdaní“ apod., přičemž různé verze pro různé zákazníky to každá musí mít jinak, resp. algoritmus si obsluha musí vybrat apod. V tomto případě se nasazuje vzor STRATEGY, kdy každý algoritmus zpracování v daném bodě je reprezentován jedním z dědiců s přepsanou metodou a výměna algoritmu v daném bodě spočívá ve velmi flexibilní výměně objektu.

 

Závěr

 

Při posouzení změn přechodu projektu od strukturovaného programování na objektově orientované programování ve stejném prostředí doporučuji zaujmout postoj, který by se dal nazvat jako „zdravý pesimismus“.

 

Snaha změnit celou filosofii všech struktur hotového strukturovaného programu reprezentuje de facto zadání na přepracování celého projektu a nejedná se určitě pouze o přeskupení neboli refactoring kódu. Je třeba proto zvážit, zda opravdu má smysl doslova zahodit stávající fungující strukturovaný systém a předělat jej ve stejném prostředí celý jinak.

 

Takový krok se ve valné většině prostě neoplatí.

 

Určitě je však vhodné zavádět struktury programu pomocí OOP (tj. objektový návrh v designu) pro nové agendy resp. při celkovém předělání celých agend tak říkajíc z gruntu. V tom případě by se již mělo postupovat podle moderních objektových postupů (objektové analytické modelování v UML, mapování do designu, použití automatů pro mapování apod.)

 

Jinou celou další oblastí pro efektivní zavedení filosofie OOP do strukturovaného programu je zavedení polymorfního chování do míst původních „neflexibilit“. Jedná se zejména o identifikaci a řešení těchto situací:

 

1.     Typové přepínače a jejich náhrada překrytím metody.

 

2.     Nasazení vzoru OBSERVER resp. delegátů pro zavedení událostí.

 

3.     Řešení akcí a jejich spouštění (vzor COMMAND).

 

4.     Výměna pouze určitých částí kódů (algoritmů) pro různé verze různých zákazníků apod. (vzor STRATEGY).

 

 

Konec článku