Ilja Kraval : Ukázka běžné asociace v modelování a v kódu v Extrémně Efektivním Modelování (19.1.2003)

Začneme jedním dotazem z diskuze Pandory:

Dobrý den,

poprosil bych místní znalce o radu s jedním problémem. Mějme např. třídu Karta zásilky, která je v asociaci s odesílající firmou a řešitelem. Třída Karta zásilky obsahuje metodu Load (int ID), která slouží pro rekonstrukci objektu z perzistentního úložiště. V této metodě je navíc rekurzívně volána metoda Load asociovaných objektů a vytvořené instance těchto tříd jsou dosazeny do privátních proměnných a dostupné prostřednictvím veřejných vlastností třídy Karta zásilky.

Výhody:

1)Objekt po zadáni ID dokáže kompletně rekonstruovat svoji strukturu a je připraven ihned k použití. Není nutné dodatečné nastavení vlastností a také tím, že objekt je zodpověný za svoji rekonstrukci, nemůže dojít k tomu, že objektu je (např. omylem) nastavena pomocí vlastnosti (konstruktoru) reference na firmu s ID odlišným od cizího klíče v databázi.

Nevýhody:

1) Jedná se o porušení principu, který říká, že asociované objekty (narozdíl od agregace) navzájem nezodpovídají za své vytvořeni (ani destrukci).

2) Pokud existuje více objektů karet, které mají za odesílatele stejnou firmu a my voláme metodu Load karty, dojde k situci, kdy máme v paměti několik objektů firmy se stejným ID, ale odlišnou objektovou referencí. Je samozřejmě možné (např. v prostředí .NET) přepsat metodu Equals a přetížit operátor rovnosti, takže i přes odlišnou objektovou referenci snadno identifikujeme identitu objektů, ale problémy samozřejmě zůstávají - množství duplicitních objektů v paměti, problémy s oboustrannými asociacemi - de facto objekt, který volá metodu Load asociovaných tříd, funguje jako "falešný" root objekt.

Možné řešení

Jako řešení mě napadlo, že ke každé třídě by se vázal "Load Manager", který by měl následující funkcionalitu.

1) Metoda Load by kontaktovala "Load Manager" se žádostí o vydání instance s daným ID.

2) "Load Manager" by prohledal privátní kolekci,kde by byly uloženy veškeré aktuální instance dané třídy, na výskyt instance s daným ID.

a) Pokud instanci s předaným ID nalezne, vrátí tuto instanci.

b)Pokud instanci nenalezne, vytvoří ji, přidá do své privátní kolekce a vrátí ji.

"Load Manager" by mohl být např. implementován jako statická (shared) metoda každé třídy nebo jako speciální objekt.

Co na to říkáte? Jak tuto situaci řešíte Vy? Díky za všechny názory.

R.S.

Rád bych na tento příspěvek odpověděl mimo jiné i z toho důvodu, že se momentálně připravuje k vydání nová technologie postupu tvorby SW nazvaná "Extrémně Efektivní Modelování" (viz článek o EFEM v této sekci serveru) a tento problém je v této technologii podrobně rozebrán v kapitole "Mapování z analytického modelování do designu".

Autor příspěvku vcelku správně pochopil, že situace, kdy jeden objekt "ovládá" druhý objekt a přitom se jedná o vztah "běžná asociace", není úplně korektní. Jak tedy má vypadat správný pohled na tuto situaci?

Pokud v analytickém modelování najdeme vztah běžné asociace, potom tím jako analytici chceme designérovi "něco sdělit". V tomto případě se jedná o lapidární sdělení typu: Prvek A vidí prvek B a může jej použít, avšak neovládá jeho život, pouze se na něj "napojí". To je podstata běžné asociace a designér tuto zásadu nenaruší. Nejčastěji bývá běžná asociace používána v kontextu tzv. číselníků (instance vidí jednu instanci z číselníku, např. auto "vidí" svoji barvu). Druhá možnost použití běžné asociace je chápána jako součást vazby v agregaci 1:N obráceně od obsaženého k majiteli. V jednom směru tedy majitel vlastní N prvků, v druhém směru vlastněný "vidí" svého majitele. Tento druh běžné asociace se však v UML nemaluje v diagramu zvlášť, protože se chápe jako obrácená vazba ve viditelnosti "navigable" od obsaženého k majiteli, kdy prvek obsažený "vidí" svého parenta, např. řádek faktury "vidí" svou fakturu, kam patří. Tedy v tomto případě je běžná asociace chápána jako součást vztahu agregace 1:N, ale v obráceném směru. Jako další známý případ použití běžné asociace je situace, kdy se zavede běžná asociace ve vztahu čistého propojení dvou entit, kdy jedna instance jednoho typu jednoduše "vidí" druhou instanci druhého typu (Faktura vidí Partnera apod.).

Pokud designér narazí na běžnou asociaci, potom si tento vztah musí namapovat do své technologie. Je samozřejmé, že v různých technologiích může vypadat namapování jinak, ale pozor, nejenom to. Velmi důležité v této úvaze je to, že způsobů, jak se tento vztah namapuje i v jedné dané technologii může být několik (!). Všechny tyto postupy by měly být pro design fázi podchyceny pomocí vzorů a pokud se nalezne další možné mapování, je třeba je také zavést do katalogu pro opětovné použití.

Uvedu dva nejčastěji používané vzory mapování běžné asociace v nejčastěji používané technologii, kterou je tzv. "hybridní technologie". Jedná se o technologii vyjádřenou jako kombinace: "OOP jazyk v aplikaci" plus "relační databáze na pozadí" (z toho důvodu název hybridní). Klasickými reprezentanty jsou například "JAVA plus ORACLE", "DOT NET plus MS SQL" a podobné kombinace.

Vzor mapování běžné asociace typu 1:1

Provádí se ve dvou krocích. První krok mapuje instance, tj. objekty v aplikaci. Druhý krok mapuje záznamy v DB. Běžná asociace se mapuje v objektech na vztah dvou tříd CA a CB a to pomocí vazební instance. Třída CA, jejíž prvek "vidí" instanci z CB, obsahuje jednu instanci z třídy CB. Název této vazební instance je plně odvoditelný z názvu role třídy CB v class modelu (například je shodný s rolí anebo se liší pouze předponou z maďarské notace apod.). Navíc třída CA obsahuje alespoň jednu metodu, která naplňuje pomocí vstupního parametru tuto instanci. Doporučuje se použít přímo property a také vyvést tuto instanci přes toto property také ven.

vzor mapování v pseudokódu JAVACIS:

class CA;
  {
   private CB mMojeB;
   ...
   public void SetMojeB(CB a_B);
   {
    ...
    mMojeB = a_B;
    ...
   }
   public CB GetMojeB();
   {
    ...
    return mMojeB;
   }
 }            
Klient používá pro naplnění běžné asociace metodu Set, pro čtení metodu Get. Pro naplnění je objekt ze třídy CA již "hotov", případně dostatečně naplněn pro použití. Objekt ze třídy CA jej neovládá a ovládá jej ten, kde drží kontext celého scénáře, tj. ten, kdo volá Set. Znamená to, že z hlediska objektu ze třídy CA jsou scénáře objektu CB nezajímavé. Vše ostatní s B se děje mimo něj.
...
CA MyB = new CB();	//jeden objekt z číselníku je vytvořen, prázdný
MyB.ID = 1001		//známe jeho ID, load děláme dvojkrokově
MyB.LoadfromDB();	// instance z číselníku inicializována 
CA NoveA = new CA(); 
NoveA.SetMojeB(MyB)  //asociace naplněna 
...
       
Jiný možný scénář:
CA MyB = ColMyB.GetItem(1001); // objekt dostaneme od manažera objektů,
                               //tj. seznamu nemusí to
                               //naplněná kolekce
CA NoveA = new CA(); 
NoveA.SetMojeB(MyB)  //asociace naplněna 
       
Poznámka: Důležité je to, že objekt A pracuje pouze s odkazem na B a to, co se s B děje, sám neovlivňuje. Běžná asociace je pouze "zápujčka ukazatele" na B.

Příklad použití Get v property (zobrazení do GUI prvku typu Label):

...
NejakyLabel.SetCaption() = NoveA.GetMojeB().NecoStringoveho();
...
	   

Nyní se vrátíme k úvahám autora dotazu o Load Manažeru. Je zřejmé, že zavrhneme myšlenku, že A ovládá B pomocí Load. Objekt A pouze dostane ukazatel na B a "nasdíli se" na něj. "Loadnutí" B se provádí mimo A. Je vidět, že úvahy autora o "Load Manažeru" jsou "na dobré cestě", jenom je třeba jej přejmenovat na seznam (není to pouze Manažer pro Load, ale obecný manažer objektů, tedy seznam). Tedy tyto úvahy mají velmi blízko ke scénáři, kdy se použije seznam jako manažer - viz druhý možný scénář v příkladech.

Druhý krok mapování běžné asociace typu 1:1 je mapování do RDB (tj. do ERD). Cizí klíč putuje z tabulky B do tabulky A. Dochází k provázání (již existujících) záznamů. Ze strany A do B nikdy nedochází ke scénářům ovládání, jako je kaskádovitý delete apod. Opět je to "zápujčka ID". Zatímco v aplikaci hraje roli zapůjčeného ukazatele vazební instance mMojeB, v datech je v úplě stejné pozici "datový ukazatel" IDMojeB. Jedná se o dvě implementace téhož nalezeného v analýze - "A vidí B" (a neovládá jej).

Vzor mapování běžné asociace typu nikoliv 1:1

V některých případech nelze ve všech situacích použít předešlého mapování 1:1 tak, jak bylo uvedeno, a to z důvodů technologických. Jedná se většinou o hromadná zpracování resp. obecněji o práci s "hromadou instancí", kdy by předešlé mapování vyžadující sekvenční zpracování (plnění asociací objekt po objektu) nemuselo podat dobrý výkon. V tom případě se stejná analytická situace mapuje na další možnosti. Uvedené činnosti "hromady instancí" se vždy zavádějí jako operace objektu Seznamu A (Manažer všech A) s požadovanými vstupními parametry a tento objekt deleguje tyto činnosti na databázi a nikoliv na svoje itemy. Toto mapování se provádí zásadně až při nedostatečné kapacitě a možnostech prvního mapování (tj. jako vynucené druhé mapování), protože vede ke snížení opětovné použitelnosti a ke zvýšenému nároku na dokumentaci. Uvedená mapování jsou jako vzory více přiblížena v návodkách k technologii EFEM, která se nyní připravuje k vydání.

Příklad: Scénář odpojení všech prvků A od určitého prvku B. Požaduje se, aby všechny prvky, které si před touto operací ukazovaly na B, si po této operaci ukazovaly na "nic" (nemají zadán item z číselníku).

Zjistí se, že sekvenční zpracování není vhodné, tj. nelze provést něco v tomto smyslu:

class CcolA;			//manažer objektů A, "živá" kolekce, 
                        //například uvnitř ní je arraylist
{
 public void Odpoj_All_Od_B (CB a_B); 
 {
  for each item in mycolA
  {
   if item.MojeB.ID = a_B.ID then
   {
   item.MojeB = Nill;
   item.Update();
   } 
  }
 }   
}
        
Tento kód již využívá naprogramovaného kódu, nastavení běžné asociace a update. Nelze tento postup použít, například pro velmi velký počet výskytů A.

Druhé možné mapování této operace a to nikoliv 1:1

       
class CcolA;		//manažer objektů A, může být "neživý"
                    //seznam(nemá za sebou živé objekty) 
{
public  void Odpoj_All_Od_B (CB	a_B); 
 { 
 DbObject.Odpoj_All_A_Od_B (a_B.ID); //voláme DB vrstvu, 
 }   
}

Činnost odpojení se implementuje až v DB, například jako SQL příkaz UPDATE s výběrovou podmínkou, kde ID daného B je vstupní parametr této podmínky.

Nevýhody: Žádný re-use, nelze použít žádný z již hotových kódů, menší transparence (předešlý kód v OOP je analyticky čitelnější), případně (v obecné rovině libovolného příkladu s libovolnou stored procedurou) možné problémy s výměnou DB. Navíc, a to je nejhorší, pokud se cokoliv změní, musíme vědět, že je třeba také zkontrolovat, zda zasahovat i do kódu zmíněného UPDATE v DB vrstvě s podmínkou a případně jej opravit. Avšak jiné cesty nemáme, protože mapování 1:1 v objektech nelze použít z důvodů technologických. U tohoto mapování se samozřejmě nevylučuje možnost použít pro jednu instanci (tam, kde se pracuje s jednou nebo málo instancemi) naplnění běžné asociace i na úrovni objektů pomocí operace objektů Set.

Je třeba se zmínit o jedné důležité okolnosti, a to, že u obou případů mapování je analytický model stejný (jako jedno zadání pro design), a tedy oba scénáře, jak první v objektech, tak druhý v databázi, vyjadřují totéž (nejsou pouze mezi sebou vzájemně použitelné). Toho využívá Extrémně Efektivní Modelování pro opravdu "efektivní dokumentaci", která umožní udržet celý projekt v konzistenci a požadované stabilitě vůči změnám při vývoji projektu a to v případě použití obou (a jiných) způsobů mapování.


V případě jakýchkoliv připomínek napište prosím na adresu objects@objects.cz