Přejít k navigační liště

Zdroják » Různé » GRASP – 4 – Polymorphism, Pure fabrication a Indirection

GRASP – 4 – Polymorphism, Pure fabrication a Indirection

Články Různé

V tomto díle o návrhových principech GRASP (General Responsibility Assignment Software Patterns) projdeme principy Polymorphism, Pure fabrication a Indirection, které se zabývají strukturními prvky architektury.

V tomto díle stručně projdeme tři principy, které poskytují vodítka k samotné struktuře kódu. Jde o několik jednoduchých zásad umožňujících nám lépe uvažovat o rozdílech mezi doménovým modelem a jeho reálnou implementací.

Pozn. V tomto díle jsem původně plánoval projít principy Information Expert a Creator, ale nakonec jsem se rozhodl z důvodu lepší návaznosti pořadí změnit.

Polymorphism

Polymorfismus je jednou ze základních vlastností objektového programování. Jde o velmi účinný nástroj pro zjednodušení kódu, který bychom měli umět správně využívat a nahradit jím méně vhodné konstrukce. GRASP formalizuje tuto zásadu jako:

Pokud chování závisí na typu objektu (třídě), přiřaďte zodpovědnost za toto chování pomocí polymorfických metod třídě, na které toto chování závisí.

Jinými slovy nikdy neřiďte chování pomocí podmínek testujících typ objektu. Místo toho zavolejte metodu objektu, která zajistí požadované chování sama. Jednotlivé podtřídy pak mohou samy ovlivnit požadované chování.

Pokud tento princip není dodržen, tak přidání každé další podtřídy vyžaduje úpravu na všech místech, kde se dříve testoval typ objektu. Na všech těchto místech by bylo nutné určit, jak se zachovat v případě, že bude zpracováván objekt nové podtřídy. Informace o chování jsou tedy roztříštěné na mnoho míst kódu (což mimo jiné porušuje princip Don’t repeat yourself – DRY).

Pokud návrh dodržuje tento princip, je rozšíření o nové podtřídy v zásadě jednoduché. Stačí vytvořit novou podtřídu, která si sama překrytím polymorfní metody určí, jaké bude chování při práci s ní. Díky tomu je možné snadno měnit chování systému vytvářením a používáním nových podtříd.

Polymorfismus se ale nemusí vztahovat čistě na vytváření podtříd. Tato zásada se vztahuje na jakoukoliv třídu, která implementuje stejné rozhraní. Návrhový vzor Proxy ukazuje, jak lze snadno při použití polymorfismu vytvořit proxy objekt, který je pak možné použít kdekoliv, kde by byl použit původní objekt, bez nutnosti jakéhokoliv zásahu. Tento proxy objekt pak může změnit chování původního objektu právě s využitím polymorfismu. Potřebná změna chování je ale opět zapouzdřena v proxy a není nutné v jiných částech kódu ošetřovat, zda pracujeme s původním objektem nebo jeho proxy.

Pokud používáme polymorfismus je nutné dodržovat Liskovové substituční princip popsaný v díle o SOLID principech. Tento princip říká, že „podtřídy by měly být zaměnitelné s jejich bázovými třídami“. Při nedodržení této zásady mi mohl původní kód přestat fungovat v případě, že by mu byla předána podtřída s nekompatibilním chováním.

Polymorfismus a abstrakce jsou také základním prostředkem pro splnění Open-closed principle popsaného ve stejném díle. Díky vytváření nových podtříd, které překrývají a mění chování abstraktních předků, je možné rozšiřovat kód o nové funkce, aniž by bylo nutné zasahovat do kódu dříve vytvořených podtříd.

S tímto principem souvisí také princip Protected variations popsaný v prvním díle o GRASP. Použití polymorfismu izoluje změny potřebné při vytvoření nové podtřídy do této podtřídy a chrání tak ostatní části systému před nežádoucími zásahy.

Z hlediska návrhu může být výhodné použít polymorfismus i v případech, kdy v aktuálním systému neexistuje potřeba měnit chování na základě typu objektu, ale je pravděpodobné, že se tato potřeba objeví v budoucnosti. Vždy bychom ale měli být realističtí ohledně pravděpodobnosti těchto změn. Pokud by ke změně nedošlo, mohlo by jít o zbytečně investované úsilí.

Tento princip je použit například v návrhových vzorech State, Strategy, Adapter, Command, Proxy a dalších.

Pure fabrication

V některých případech by přiřazení odpovědnosti některé třídě představující objekt z problémové domény narušilo soudržnost (cohesion) této třídy, zvýšilo její provázání (coupling) nebo porušilo jiné principy. V takovém případě je vhodné vytvořit novou třídu, která nepředstavuje nic z oblasti domény – třídu vytvořenou pouze pro zlepšení kvality návrhu.

Takovéto třídy nazýváme pure fabrication (neexistuje žádný vhodný český překlad, významově by asi bylo nejbližší něco jako „pouhé konstrukční prvky“).

Třídy představující pure fabrication nereprezentují nic, co by existovalo v rámci problémové domény. Tyto třídy tedy nenajdeme v doménovém modelu vzniklém při analýze požadavků. Jde o třídy vytvořené čistě pro účely architektury software, které jsou identifikovány až při zpracování návrhu.

Typickým příkladem takovýchto tříd jsou třídy pro práci s databází. V problémové doméně nic jako databáze neexistuje, ale přiřazení odpovědnosti za perzistentní uložení každé třídě domény zvlášť by vedlo k nekvalitnímu návrhu. Jsou proto vytvořeny třídy pure fabrication, které tyto odpovědnosti převezmou.

Může jít také o třídy seskupující související chování, které sice je v doméně nějakým způsobem reprezentováno, ale jsou důvody, proč jej extrahovat do oddělené třídy. Může jít například o různé pomocné třídy nebo algoritmy.

Pure fabrication by ale mělo být používáno, pouze pokud ostatní řešení, která nevyžadují pure fabrication, mají jasné nevýhody. Vytváření pure fabrication je totiž obvykle v rozporu s principem Information expert (viz příští díl). Odděluje totiž zpracování informací od objektů, které tyto informace ukládají. Pokud by bylo použito do extrému, vedlo by k návrhu, který by v podstatě představoval procedurální programování, kde by funkce byly pouze seskupeny do objektů.

Většina návrhových vzorů představuje pure fabrication.

Indirection

Jak přiřadit zodpovědnosti, pokud se potřebujeme vyhnout přímé vazbě? Přiřaďte zodpovědnost prostředníkovi, který zprostředkuje propojení mezi prvky, takže nebudou muset být propojeny přímo.

Hlavní motivace pro nepřímá propojení je snižování provázanosti (coupling). Třídy používané jako prostředníci představují pure fabrication.

Larman uvádí staré pořekadlo OO návrhu: „Většina problémů může být vyřešena přidáním další úrovně nepřímosti.“ Zde je ovšem nutné také uvést komplementární poučku: „Většina problémů s výkonem může být vyřešena ubráním nějaké úrovně nepřímosti.“

Použití nepřímých vazeb snižuje provázání kódu. Třídy zachycující chování systému spolu nemusejí být vůbec provázány. Tyto nezbytné vazby jsou zachyceny v prostřednících. Při změně vazeb pak obvykle stačí upravit prostředníka a není nutné upravovat všechny třídy, které by závisely přímo. Díky tomu je snížen dopad změn a dodržen princip Protected variantions.

Opět stejně jako u pure fabrication platí, že většina návrhových vzorů je realizací této zásady. Mezi nejtypičtější příklady patří vzory Adapter, Bridge a Facade. Ale i další návrhové vzory tento princip využívají – například Abstract Factory představuje prostředníka mezi uživatelem a konstruktorem.

Pokračování příště

V dalším díle se podíváme na principy Information Expert – Informační expert a Creator – Tvůrce, které představují základní principy pomáhající při výběru objektů pro přidělení zodpovědnosti podle GRASP.

Zdroje a další informace

  • Craig Larman: Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and the Unified Process (2nd Edition), Prentice Hall PTR, 2001, ISBN 978–0130925695
  • http://en.wikipedia.org/wi­ki/GRASP_(object-oriented_design)
  • Robert C. Martin: Čistý kód, Computer Press a.s., 2009, ISBN-978–80–251–2285–3
  • http://voho.cz/wiki/infor­matika/oop/princip-grasp/
  • Glenford J. Meyers: Reliable software through composite design, Petrocelli/Charter, 1975, ISBN 978–0884052845
  • Rudolf Pecinovský: Návrhové vzory, Computer Press a.s., 2007, ISBN-978–80–251–1582–4
  • Kirk Pepperdine: Nevyužité příležitosti k polymorfismu, 97 klíčových znalostí programátora, Computer Press a.s., 2010, ISBN-978–80–251–3145–9 [EN verze na webu]

Komentáře

Subscribe
Upozornit na
guest
4 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Radek Miček

Jinými slovy nikdy neřiďte chování pomocí podmínek testujících typ objektu.

To se ale běžně dělá třeba v případě výjimek.

Pokud tento princip není dodržen, tak přidání každé další podtřídy vyžaduje úpravu na všech místech, kde se dříve testoval typ objektu.

Tahle skutečnost je určitým způsobem symetrická ke skutečnosti, že po přidání abstraktní metody do bázové třídy v jazycích jako C# nebo Java musíme upravit podtřídy, jenž nemají být abstaktní. Stejně jako v případě abstraktních podtříd může kompilátor upozornit na místa, kde testujeme typy objektů a chybí tam test na nějaký (nově přidaný) typ.

Informace o chování jsou tedy roztříštěné na mnoho míst kódu (což mimo jiné porušuje princip Don’t repeat yourself – DRY).

Pokud se kód neopakuje, tak DRY není porušen.

Tento princip říká, že „podtřídy by měly být zaměnitelné s jejich bázovými třídami“.

LSP mluví o podtypech a ne o podtřídách. Koneckonců aby nějaký typ byl podtyp, tak to nemusí být podtřída (a naopak) – bohužel v mainstreamových staticky typovaných jazycích musí.

Martin Jonáš

Jako každé pravidlo i polymorfismus má svoje výjimky. V případě zachycování výjimek je typ výjimky hlavním a v podstatě jediným nositelem informace o tom co se stalo.

To že existují případy vyžadující nevyhnutelnou úpravu na mnoha místech kódu přece neznamená, že se jim nemáme snažit vyhnout. V mnoha případech (vývoj knihoven) pak často ani nemáme nad všemi užitími našich tříd kontrolu.

DRY bývá porušeno v případech, kdy na mnoha místech používáme stejnou sadu podmínek kontrolujících typ. Jde tedy obvykle o opakující se kód. Opakující se informací je právě seznam známých podtříd.

S podtypy/podtřídami v LSP máte pravdu.

Radek Miček

To že existují případy vyžadující nevyhnutelnou úpravu na mnoha místech kódu přece neznamená, že se jim nemáme snažit vyhnout.

S tím souhlasím. Představte si, že mám třídy pro reprezentaci aritmetických výrazů (jazyk C#, ale závěr se vztahuje i na Javu a jiné jazyky):

public abstract class Expr { /* obsahuje pripadne abstraktni metody */ }
public class ExprAdd : Expr { /* uzel reprezentujici scitani */ }
public class ExprInt : Expr { /* uzel reprezentujici cislo */ }

Mám minimálně 2 možnosti, jak implementovat vyhodnocení výrazu:

  • Do třídy Expr přidám abstraktní metodu eval, kterou implementuji ve třídách ExprAdd a ExprInt.
  • Nebo vytvořím někde stranou funkci eval, která udělá switch podle typu ExprAdd/ExprInt a provede příslušné vyhodnocení (pokud někomu vadí switch podle typu, tak si místo toho může myslet Visitor – tj. Eval bude třída).

Nevýhoda první možnosti je v tom, že musím upravit kód na mnoha místech – ve třídách ExprAdd a ExprInt. Navíc je logika vyhodnocení výrazu rozházena po mnoha třídách.

Nevýhoda druhé možnosti je v tom, že pokud přidám např. ExprMult (uzel pro násobení), tak musím upravit i funkci eval a každou další funkci, která dělá switch dle typu, takže opět musím upravit kód na mnoha místech.

Pokud nebudu přidávat další uzly, tak je druhá možnost lepší. Pokud nebudu přidávat další metody, tak je první možnost lepší. Programátor by se měl tedy rozhodnout podle toho, zda bude přidávat nové metody nebo nové uzly.

Martin Jonáš

jak říkáte, vždycky je potřeba se zamyslet nad výhodami a nevýhodami jednotlivých řešení. GRASP principy je vždycky potřeba zvažovat jako celek a dle předpokládaného vývoje.

Osobně bych ale volil první možnost, protože přidání nového typu výrazu považuji za pravděpodobnější než přidání nové metody do tříd výrazů. (Protected variations)

Zásada Polymorphism zde jasně preferuje první možnost.

Dle principu Information Expert (bude probrán příště) by zodpovědnost za výpočet měla mít třída, která má potřebné informace. V našem případě tedy opět třída výrazu.

Třída Eval by představovala Pure fabrication, kterému bychom se měli vyhnout pokud k tomu není nějaký důvod. Ten by zde ale je možné představit například v efektivitě výpočtu nebo jak píšete v rozhození logiky výpočtu na mnoho míst.

V zásadě jde o to, že GRASP nám nikdy nedá jasnou odpověď jak to udělat, ale poskytuje vodítka jak o tom jasněji uvažovat. Volba je ale vždy na vývojáři.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.