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

Zdroják » Různé » GRASP – 7– Modelové příklady

GRASP – 7– Modelové příklady

Články Různé

V závěrečném díle o návrhových principech GRASP (General Responsibility Assignment Software Patterns) si ukážeme několik modelových příkladů aplikace GRASP.

Ještě než se vrhneme na modelové případy, jen několik stručných poznámek k pojmu viditelnosti objektů, který budeme dále používat.

Viditelnost

Viditelnost (visibility) je schopnost jednoho objektu vidět (mít referenci) na jiný objekt. Je základním předpokladem pro posílání zpráv (volání metod).

Rozlišujeme 4 základní typy viditelnosti (viditelnost objektu A z objektu B):

  • Viditelnost atributu (attribute visibility)
    • Objekt A je atributem objektu B
    • Nejčastější typ viditelnosti v OOP systémech
    • Viditelnost je dlouhodobá (často existuje po celou dobu existence objektů A a B)
  • Viditelnost parametru (parameter visibility)
    • Objekt A je parametrem metody objektu B
    • Viditelnost je pouze dočasná (trvá jen po dobu provádění metody)
    • Často se viditelnost parametru mění na viditelnost atributu. Jednoduše tím, že si referenci obdrženou v parametru uložíme do atributu pro pozdější použití.
  • Lokální viditelnost (local visibility)
    • Objekt A je lokálním objektem (ne parametrem) v metodě objektu B (při provádění metody je vytvořen nový objekt B nebo je objekt B vrácen jako výsledek volání nějaké metody).
    • Viditelnost je pouze dočasná (trvá jen od vzniku objektu do konce provádění metody).
    • Stejně jako viditelnost parametru je možné ji transformovat na viditelnost atributu.
  • Globální viditelnost (global visibility)
    • Objekt A je nějakým způsobem globálně dostupný, aniž bychom museli referenci na něj nějak obdržet  (například. globální proměnná, Singleton, statické atributy  a podobně)
    • Tento způsob je v OOP nejméně častý a měli bychom se mu vyhýbat, protože do kódu zanáší skryté závislosti, které nejsou na první pohled viditelné.

Viditelnost objektů je často limitujícím faktorem při přiřazování zodpovědností. Pokud má mít objekt přiřazenu nějakou zodpovědnost, je nutné, aby měl viditelnost na všechny objekty, které k provedení této zodpovědnosti potřebuje. To může vyústit v předávání mnoha parametrů a tím k zvyšování provázanosti.

Modelový příklad

Uvedený modelový příklad volně vychází z příkladů, které používá Craig Larman.  Příklad představuje část systému e-shopu, která řeší evidenci objednávek a zpracování plateb.

Jde pouze o modelový příklad, který řadu věcí zjednodušuje. Cílem je ukázat, jakým způsobem se při návrhu používají principy GRASP a jakým způsobem při návrhu uvažovat, nikoli ukázat ideální návrh architektury e-shopu.

Předpokládáme, že v dřívějších fázích návrhu jsme již navrhli následující třídy:

  • Register – představuje celý sub-systém evidence objednávek a plateb, udržuje přehled všech objednávek
  • ProductCatalog – katalog produktů obsahující informace o všech prodávaných produktech
  • ProductSpecification – obsahuje informace o daném typu produktu jako je například název, katalogové číslo a cena
  • Sale – konkrétní objednávka (obsahuje seznam položek objednávky,  její stav a případně odkaz na přijatou platbu za tuto objednávku)
  • SaleLineItem – položka objednávky (počet kusů daného typu produktu)
  • Payment – příchozí platba (přijatá částka)
  • PaymentsHistory – evidence příchozích plateb

Nyní postupně projdeme několik příkladů, jak vyřešit přiřazení dalších odpovědností těmto třídám.

Vytvoření nové objednávky

Vytvoření nové objednávky musí zahrnovat několik kroků:

  • Vytvoření instance třídy Sale představující objednávku
  • Zařazení instance Sale do evidence v Register
  • Atributy instance Sale musí být inicializovány na správné hodnoty

Prvním krokem bude určení toho, který objekt bude pro událost vytvoření objednávky fungovat jako controller – tedy objekt, který přijme prvotní zprávu o požadavku na provedení dané činnosti.

Pozn.: V tomto případě se nejedná o controller přijímající systémové události přímo od uživatelů z vnějšku systému, ale o controller v rámci modelovaného sub-systému.

Podle GRASP principu Controller je nejvhodnějším kandidátem objekt, který představuje celý sub-systém v našem případě tedy objekt Register. K sub-systému tedy budou jiné části systému přistupovat prostřednictvím objektu Register, který před nimi částečně ukryje vnitřní implementaci sub-systému. Register v tomto případě slouží jako Facade a podporuje princip Indirection.

Následně musíme rozhodnout, kdo bude zodpovědný za vytvoření instance Sale. Protože jde o vytváření instancí, budeme se řídit podle principu Creator. Ten doporučuje přiřadit zodpovědnost za vytváření instancí třídě, která agreguje nebo zaznamenává vytvářené objekty. V našem příkladu přehled o instancích Sale zaznamenává objekt Register. Princip Creator také doporučuje, aby instance vytvářel objekt, který k tomu má všechny dostupné informace (Information expert). Vzhledem k tomu, že Register byl určen jako objekt, který přijme požadavek na vytvoření objednávky, má informace o tom, jaká objednávka má být vytvořena. V obou případech je tedy kandidátem na vytvoření instance Sale objekt Register a tuto zodpovědnost tedy přiřadíme jemu.

Zařazení instance Sale do evidence pak objekt Register provede sám hned po jejím vytvoření.

Zbývá tedy vyřešit inicializaci atributů nově vytvořené objednávky. Tato inicializace bude mimo jiné obsahovat vytvoření prázdné kolekce položek objednávky. Zde opět uplatníme princip Creator a tuto zodpovědnost přiřadíme třídě, která tuto kolekci obsahuje – tedy samotné třídě Sale. Ostatní parametry objednávky jako například její číslo a podobně by pak dle principu Information expert měl nastavit objekt, který k tomu má dostupné informace. Tím je vnašem případě Register, který má jak referenci na objekt Sale, tak obdržel informace o tom, jaká objednávka má být vytvořena.

Přidání položek objednávky

Přidání položky do objednávky musí zahrnovat několik kroků:

  • Vytvoření instance třídy SaleLineItem představující jednu položku objednávky
  • Přidání instance SaleLineItem do příslušné instance Sale
  • Atributy instance SaleLineItem musí být inicializovány na správné hodnoty (počet a reference na ProductSpecification)

Opět musíme určit controller – tedy bod, kde náš subsystém obdrží instrukce k přidání položky do objednávky, kterým bude opět objekt Register.

Zodpovědnost za vytvoření instance SaleLineItem přiřadíme podle principu Creator třídě, která tyto položky agreguje – tedy třídě Sale. Ta zároveň provede i přidání nové instance SaleLineItem do svého seznamu položek objednávky.

Zajímavou částí tohoto příkladu je nastavení atributů SaleLineItem. Controller je v tomto případě Register, ale instance se vytváří v Sale. Proto je třeba, aby při volání příslušné metody třídy Sale objekt Register předal informaci o počtu kusů.

Pro inicializaci atributů SaleLineItem musíme také nějak získat referenci na ProductSpecification. Register obdrží pouze informaci o katalogovém čísle produktu, který má být přidán. Musíme tedy podle tohoto katalogového čísla získat příslušnou instanci obsahující dodatečné informace (například cenu).

Zodpovědnost za získání instance ProductSpecification dle katalogového čísla přiřadíme podle principu Information expert. Potřebné informace o všech typech produktů má v našem příkladu objekt ProductCatalog – bude to tedy jeho zodpovědnost.

Na přidání položky do objednávky podílejí dva objekty, Register a Sale. Který z nich by však měl být zodpovědný za spolupráci s ProductCatalog? Aby mohl některý z těchto objektů požádat ProductCatalog o nalezení instance ProductSpecification, musí pro něj být viditelný. Musíme tedy rozhodnout, kterému objektu tuto viditelnost poskytneme a v jaké podobě.

Z hlediska Low coupling se jako vhodnější kandidát jeví Register. V případě zvolení Sale bychom totiž museli zajistit, aby pro Sale byl ProductCatalog viditelný. To bychom museli zajistit nastavením reference na ProductCatalog do každé Sale, což by nutně musel provádět Register. Protože v obou případech by vzniklo provázání Register-ProductCatalog, je vhodnější takové řešení, kde nevznikne zbytečné provázání Sale-ProductCatalog.

Výpočet celkové ceny objednávky

Nyní si projdeme, jakým způsobem by měl probíhat výpočet celkové ceny objednávky poté, co do ní přidáme všechny položky.

První otázkou je, kdo by měl mít zodpovědnost za vrácení celkové ceny objednávky. Podle Information expert by to měla být Sale sama. Toto rozhodnutí podporuje i Low coupling – vnitřní objekty SaleLineItem není nutné předávat nikam ven a tím zvyšovat provázání.

Sale svoji celkovou cenu spočítá jako součet celkových cen jednotlivých SaleLineItem.

Odlišné řešení nám však doporučuje princip High cohesion. Pokud výpočet celkové ceny přidáme do třídy Sale, bude tato třída dělat více věcí a bude tak mít menší soudržnost. Řešením by tedy bylo výpočet ceny oddělit do samostatné třídy např. PriceCalculation představující Pure fabrication, která by řešila pouze výpočet.

Musíme tedy zvážit, zda snížení soudržnosti třídy Sale je dostatečným důvodem, proč zvýšit provázání a zavést Pure fabrication třídu. V tomto případě tomu tak nejspíše není, ale měli bychom být na pozoru před přílišným nafukováním třídy Sale při přidávání dalších zodpovědností.

Obdobně jako u SaleSaleLineItem by měla umět svoji celkovou cenu spočítat sama, protože je pro tento úkol Information expert. Pro výpočet stačí prostě provést součin počtu kusů a ceny za jeden kus uvedené v ProductSpecification, který SaleLineItem vidí ve svém atributu.

Toto uspořádání podporují i principy Protected variations a Polymorphism. Pokud by vznikly případy, kdy je cena položky počítána jinak – například množstevní sleva. Stačilo by vytvořit podtřídu SaleLineItem obsahující upravený algoritmus výpočtu. Takováto změna by se jiných částí systému nedotkla.

Přijetí platby za objednávku

Jako poslední si projdeme řešení přijetí platby za objednávku. Náš subsystém obdrží informaci o přijetí platby za objednávku s daným číslem a zaplacenou částku. Zpracování platby pak bude zahrnovat následující kroky

  • Vytvoření instance Payment
  • Nastavení instance Payment na hodnotu přijaté částky
  • Přiřazení instance Payment k příslušné instanci Sale
  • Ověření zda přijatá částka odpovídá ceně objednávky
  • Uložení instance Payment do PaymentsHistory

Opět bude jako controller, který obdrží informaci o provedené platbě, fungovat objekt Register.

Prvním krokem je vytvoření instance třídy Payment. Podle principu Creator máme dva kandidáty. Prvním je objekt Sale, který bude Payment obsahovat. Druhým je objekt Register, který má inicializační data (přijatou částku).

Mezi těmito dvěma možnostmi se tedy musíme rozhodnout dle dalších principů. Musíme zajistit, aby zvolená možnost co nejlépe splňovala Low coupling, High cohesion a Protected variations.

Pokud se objekt Payment bude vytvářet v Sale, objekt RegisterPayment vůbec nemusí vědět, což podporuje Low coupling. SalePayment musí vědět v každém případě, protože jej obsahuje.

Instanci Payment, ale musíme ale také uložit do PaymentsHistory. Musíme tedy vyřešit i otázku, kdo vytvořený objekt Payment do evidence uloží. Opět se zde potýkáme s problémem viditelnosti. Pokud by instance Payment byla vytvářena v Sale musela by Sale mít viditelnost na PaymentsHistory. Vzniklo by tedy provázání SalePaymentsHistory. Vhodnější by tedy bylo, kdyby Payment do PaymentsHistory uložil Register. Ten by s PaymentsHistory byl provázán v každém případě, protože by musel zajistit předání viditelnosti na PaymentsHistory do Sale po jejím vytvoření.

Vznikne nám tedy buď provázání SalePaymentsHistory (pokud bude instanci Payment vytvářet Sale) nebo RegisterPayment (pokud instanci Payment vytvoří Register). Dle Protected variations jeví jako menší zlo provázání  RegisterPayment. Pokud bychom totiž chtěli do budoucna přidat další objekty, které potřebují z nějakého důvodu obdržet Payment, bylo by je vždy nutné provázat jak s Register, tak se Sale.

Zbývá nám vyřešit poslední krok a tím je ověření zda přijatá částka odpovídá ceně objednávky. Princip Information expert říká, že zpracování by měla provést třída, které k tomu má dostupné informace. Zde je však nutné porovnat hodnotu uloženou v Payment (přijatá částka) s celkovou cenou objednávky, kterou ví Sale. Která z těchto dvou tříd by tedy toto ověření měla provést?

Odpověď nám dá princip Low coupling. Třída Sale již nyní má viditelnost na Payment a je s ní tedy provázána. Naopak to ale neplatí a třída Payment je na Sale nezávislá. Pokud by ověření měla provést Payment bylo by nutné zajistit, aby měla viditelnost na Sale, a tím přidat další provázání.

Závěr

Principy GRASP probírané v posledních dílech seriálu by měly sloužit jako pomůcka při rozhodování o přidělování zodpovědností. Pomáhají také jako společný slovník pojmů, umožňující zjednodušit diskuze o jednotlivých variantách návrhu.

Při návrhu není možné jednotlivé principy uvažovat samostatně, protože často jsou jejich požadavky proti sobě.  Správný způsob, jak tyto principy používat, je tedy ve zvážení všech pro a proti jednotlivých variant z pohledu všech principů GRASP.

Zdroje

Komentáře

Subscribe
Upozornit na
guest
7 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
?

Proč v příkladu nevytváří instanci Payment objekt PaymentsHistory, když je agregátorem instancí Payment? Stejný případe je ProductCatalog – je agregátorem ProductSpecifi­cation a bere za ně odpovědnost (dohledává je podle čísla a jistě by je i vytvářel, kdyby to bylo v příkladu řešeno).
Je nějaký důvod, proč tuto variantu při rozhodování o přidělení zodpovědnosti za vytváření instance Payment autor vůbec neuvažoval, nebo na ni prostě jen zapomněl?

Mystik_7

Přiznávám, že tuhle možnost jsem opravdu zapomněl :-)

Podle principu Creator by instanci Payment opravdu mohl vytvářet i objekt PaymentsHistory, protože tyto objekty zaznamenává.

Toto řešení by ale znamenalo, že inicializační data musí být předána z Register do PaymentsHistory a vzniklá instance vrácena zpátky do Register. Register by tedy pouze delegoval zodpovědnost za vytvoření Payment na PaymentsHistory.

?

Díky za odpověď. Osobně bych to viděl spíš jako věc náhledu na PaymentHistory – pokud je to jen jakýsi log proběhlých operací, tak asi nemá smysl mu zodpovědnost za vytváření přidávat. Pokud je to ale plnohodnotný agregátor, ve kterém chci platby skladovat, dohledávat a podobně, tak by to podle mne smysl mělo.
To mi říká jen nějaký cit nebo zkušenost. Explicitně říct, podle kterých z těch návrhových principů by se to tak určilo by mohlo být také zajímavé.
Zřejmě by se tím (oproti vytváření přímo v Register) zvýšila soudržnost ale zároveň i zvýšila provázanost, ne?

Mystik_7

Navrhované řešení by zlepšilo soudržnost třídy Register. U Třídy PaymentsHistory by záleželo jak píšete na tom co tato třída představuje. Pokud by představovala kompletní agregátor plateb bylo by vytváření objektů plateb v souladu s ostatními zodpovědnostmi a soudržnost by tedy byla zachována. Pokud by ale představovala jen log plateb pak by po změně měla dvě už ne tolik soudržné odpovědnosti – logování plateb a vytváření plateb podle příchozích informací.

Provázanost by se zvýšila, protože Register by byl odkázán na PaymentsHistory a nebyl by schopen zpracovat platbu bez její účasti. pokud bychom například chtěli logování zrušit nebo nahradit jiným způsobem byla by úprava obtížnější.

Další možností by bylo použít Paymentshistory jako controller pro přijetí platby. Tj. informaci o přijaté platbě by dostal PaymentsHistory, který by vytvořil objekt Payment a pak jen zavolal Register a objekt mu předal. Tím by bylo zrušené provázání Register na PaymentsHistory. Mohlo by to ale mít negativní důsledky na okolí součásti systému.

?

Díky za odpověď a také za celý zajímavý seriál.

Podbi

Díky za zajímavý seriál i ochotné odpovědi v diskusi! Rád jsem jej četl.

Rob

Osobně zastávám názor že aplikační logika patří spíše do trigerů a uložených procedur, nicméně takhle hezky a srozumitelně napsaný článek jsem dloho nečetl. Super

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.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.