GRASP – 7– Modelové příklady

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.

Seriál: Principy objektově orientovaného návrhu (12 dílů)

  1. Návrhové principy: SOLID 9.5.2012
  2. Návrhové principy: Deméteřin zákon 18.5.2012
  3. Návrhové principy: DRY 30.5.2012
  4. GRASP – 1 – Úvod a Protected variations 20.6.2012
  5. GRASP – 2 – High cohesion 2.7.2012
  6. GRASP – 3 – Low coupling 16.7.2012
  7. GRASP – 4 – Polymorphism, Pure fabrication a Indirection 30.7.2012
  8. GRASP – 5 – Information expert a Creator 15.8.2012
  9. GRASP – 6 – Controller a jak GRASP používat 17.9.2012
  10. GRASP – 7– Modelové příklady 15.10.2012
  11. Návrhové principy: Tvorba balíčků (1/2) – Soudržnost 14.11.2012
  12. Návrhové principy: Tvorba balíčků (2/2) – Závislosti a provázanost 14.1.2013

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

Martin Jonáš pracuje jako projektový manažer v GrowJOB institute. Vystudoval Informační systémy na Fakultě informačních technologií VUT.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 7

Přehled komentářů

? Re: GRASP – 7– Modelové příklady
Mystik_7 Re: GRASP – 7– Modelové příklady
? Re: GRASP – 7– Modelové příklady
Mystik_7 Re: GRASP – 7– Modelové příklady
? Re: GRASP – 7– Modelové příklady
Podbi Poděkování
Rob Fakt pěkně napsaný
Zdroj: https://www.zdrojak.cz/?p=3727