\

Školení Návrhové vzory, OOP a UML


 Sunday, February 05, 2006
Ukázka použití třídy BusinessObjectBase

Máme-li napsanou třídu BusinessObjectBase, která hraje roli společného předka všech objektů v business vrstvě všech našich aplikací, je čas ukázat, jak vypadají typičtí potomci.



Předchozí spoty

Bázová třída pro business objekty - návrhový vzor Layer Supertype
Cachování řádků z databáze pro business objekty - třída DataCacheHelper

Zobrazit kód

Třída Order reprezentuje v našem systému objednávku. U objednávky nás zajímá, jakému patří zákazníkovi (vlastnost Customer), rozhodli jsme se, že budeme vyžadovat u každé instance objednávky popisek (vlastnost Description) a samořejmě samotná objednávka je složena z položek objednávky (kolekce Items). Položkou objednávky budeme rozumět třídu, v níž je uloženo, jaké zboží a kolik kusů si zákazník objednal a celková cena položky (kusová cena objednaného zboží * počet). Celková cena objednávky je sumou cen jednotlivých položek. Pro jednoduchost vidíte v kódu zatím jen třídu Objednávka.

  1. Třída Objednávka má dva konstruktory. Prvni konstruktor, přijímající popisek objednávky a zákazníka, vyvolává bezparamerický konstruktor třídy BusinessObjectBase. Jak jsem psal v předchozím spotu, bezparametrický konstruktor slouží k sestrojení objektů bez obrazu v databázi. Chcete-li založit novou objednávku, stačí když si uložíte odkazy na objekty předané v konstruktoru a zbytek práce delegujete na BusinessObjectBase, která nastaví všechny potřebné příznaky nutné pro práci s objektem bez perzistence. Druhý konstruktor přijímá Id (unikátní identifikátor) objednávky, která je uložena v databázi. Jeho tělo je prázdné, veškerou činnost provádí BusinessObjectBase - nastavení příznaků a uložení Id pro pozdější nahrání objektu z databáze. Objekt je po vykonání konstruktoru "prázdnou schránkou", duchem (GHOST), který načte svá data z databáze teprve tehdy, když použijeme některou metodu nebo vlastnost pracující s instančními a perzistovanými proměnnými. Z hlediska uživatele naší třídy (myšleno programátora)  je ale vše ve starých kolejích - rozhraní objektu bude reagovat tak, jak očekává,  a existenci pozdní incializace objektu (Lazy Load) může ignorovat. Poslední větu si přečtěte ještě jednou a tiše s k ní přidejte pravidlo: Nikdy nesmím nutit uživatele svých tříd volat nějaké speciální "Init" metody předtím, než začnou s třídou pracovat. Jedinou legální "Init" metodou je konstruktor.
  2.  Jak k pozdní incializaci a tedy nahrání dat objektu dojde? Vlastnosti, jejichž hodnoty jsou uložené v databázi (Id zákazníka jako klíč v tabulce Objednávek a popisek (Description) objednávky) mají na prvním řádku svých get/set přístupových metod volání metody TriggerLoad. Jak víme, jde o volání metody z BusinessObjectBase, která sama rozhoduje, zda již byla data objektu nahrána z databáze. Pokud data nebyla nahrána, třída BusinessObjectBase řídí scénář nahrávání objektu v metodě Load. Jestliže jsou data pro objednávku v třídě DataCacheHelper, je volána metoda DoInternalLoad(DataRow row), která načte data objednávky z předaného řádku. Když v třídě DataCacheHelper data pro aktuální instanci nemáme, BusinessObjectBase volá variantu metody DoInternalLoad bez argumentů. Metoda DoInternalLoad vyzvedne řádek z databáze s využitím databázové komponenty (což je Singleton nebo Thread specific storage - jednoduše třída zapoudřující  API pro práci s datovým zdrojem) a tento řádek předá své stejně nazvané sestřičce, o níž byla řeč výše.
    Důležité je, že celý scénář nahrávání řídí bázová třída - v odvozené třídě jen v přesně vymezených bodech scénáře "dosazuje" třída Order své vlastní specifické chování a používá své speciální atributy.
  3. V set přístupových metodách u vlastností využíváme metodu CheckEquals z BusinessObjectBase. Jak už víte z minulého spotu, metoda CheckEquals zkontroluje rovnost dvou objektů a podle toho nastaví příznak IsDirty - byl objekt změněn proti datům v databázi a musí tedy dojít k jeho uložení?
  4. Jak je to nahráním položek objednávky  - vlastnost Items? Často se stává, že potřebujete pracovat se samotným objektem, ale je zbytečné při nahrání dat objektu  z databáze ihned plnit i všechny jeho kolekce. Kolekce Items je naplněna při prvním přístupu ke kolekci  - příznak, zda byla kolekce nahrána nese pomocná instanční proměnná m_itemsLoaded. Jestliže kolekce naplněna nebyla, je volána privátní metoda loadItems, v níž s pomocí databázové komponenty vyzvedneme z databáte všechny položky objednávky - filtrem, který je použit v SQL dotazu je samozřejmě Id objednávky. Poté projdeme vrácenou tabulku a všechny řádky uložíme do třídy DataCacheHelper. Zde přepokládáme, že metoda DbComponent.Instance.OrderItem_GetByOrderId, vrací z databáze všechna data potřebná pro obnovení objektu z databáze, nejen jejich Id. To je běžná praxe, pokud máte uloženou proceduru, která vybírá záznamy z jedné tabulky (pro jednu třídu) podle různých podmínek. Důležité je, že za pomoci identitní mapy (Identity Map) sestrojíme objekty - "duchy" - OrderItem, kterým předáme jejich Id,  a třída BusinessObjectBase již zajistí, že když instance OrderItem budou chtít nahrát svá data z databáze, tak jí bude vrácen řádek uložený v instanci třídy DataCacheHeper. To znamená, že třída OrderItem nebude zbytečně zatěžovat databázi duplicitními dotazy a přitom kontrola na existenci cachovaného řádku pro instance jakékoli třídy je součástí společného předka BusinessObjectBase a "nezasviníme" si stejným kódem celou business vrstvu.
  5. Metoda Order také přepisuje metodu DoInternalSave, v níž uloží své atributy do databáze. Jak uvidíme dále, metodě Order_Update předáváme hodnotu zděděného a bázovou třídou spravovaného příznaku IsNew - tento příznak metoda interně použije k rozhodnutí, zda provede SQL příkaz INSERT nebo UPDATE1. Opět - po volání metody Save uživatelem jen třída BusinessObjectBase jakožto finální instance rozhoduje, zda je potřeba data ukládat a zda je tedy vubec nutné a účelné volat metodu DoInternalSave. Opět vidíte čistý řez mezi kontrolou, kdy je nutné objekt uložit (třída BusinessObjectBase),  a samotným ukládáním (třída Order i další potomci BusinessObjectBase). Kromě uložení vlastních hodnot odpovídá třída za uložení agregovaných objektů OrderItem. Proto v metodě DoInternalSave nejprve zkontroluje, jestli byly objekty v kolekci nahrány (příznak m_itemsLoaded) a pokud ano, tak jen zavolá pomocnou metodu SaveCollection, která je implementována, jak jinak že;), v třídě BusinessObjectBase.

I když z příkladu byste měli začít tušit, proč je BusinessObjectBase tak výhodná, stále neřešíme některé problémy.

  1. V příkladu není vůbec řešeno odebírání a přidávání položek do kolekce - to znamená nastavování/zrušení "rodiče" u agregovaných objektů. Nijak jsme neřešili m:n relace a odpovědnost za zakládání/rušení záznamů ve vazebních tabulkách. Myslíte, že i zde půjde využít třída BusinesObjectBase? ;)
  2. Co když si budeme chtít u některých potomků  BusinessObjectBase "vynutit" jiné chování - třeba zamezit použití DataCacheHelperu?
  3. A co transakce? V našem příkladu zatím nijak neřešíme ukládání objektů v transakci, což bychom měli, protože určitě nechceme mít v systému objednávky s polovinou objednaných položek. Jaký objekt má spouštět a řídit transakci? Kdo musí ošetřit chyby vzniklé při ukládání objektu?

To be continued... :)

Poznámky:

  1. Jsem si vědom střídavé soudržnosti metody související s "přepínačem" IsNew . V dalších dílech bude vysvětleno, proč střídavá soudržnost zrovna u této metody nevadí.


Sunday, February 05, 2006 2:44:36 PM (Central Europe Standard Time, UTC+01:00)       
Comments [7]  Návrhové vzory