Po opakovaných přáních poodkrýt zákoutí našeho frameworku a generované business-vrstvy jsem se rozhodl zveřejnit vybraná témata a perličky. Dnes přicházím s BusinessObjectCollection, resp. s možným způsobem implementace kolekcí v business-vrstvě pomocí generik tak, abychom dosáhli maximální kompatibility typů a nemuseli bojovat s neustálou typovou neshodou kolekcí.
Čeho chceme u kolekcí business-objektů dosáhnout:
- Chceme jednu společnou bázovou třídu BusinessObjectCollection pro všechny kolekce business-objektů a v ní centrálně implementovat operace, které jsou společné pro všechny business-objekty. Pro příklad tam dáme primitivní metodu SaveAll(), která všechny business-objekty v kolekci uloží a její implementace je stejná, ať už se jedná o kolekci Faktur, nebo kolekci Subjektů.
- V bázové třídě BusinessObjectCollection chceme definovat i operace, které pracují přímo s business-objekty, pokud možno strong-type, typem co nejpřesnějším. Např. běžné Add(Order order), Remove(Subject subject), atp.
- V bázové třídě BusinessObjectCollection chceme definovat i operace, které pracují s kolekcemi business-objektů, pokud možno strong-type, typem kolekce co nejpřesnějším. Např. FindAll(), který vrací kolekci všech nalezených prvků odpovídajících nějaké podmínce, atp.
- Od této společné bázové abstraktní třídy BusinessObjectCollection chceme děděním vytvářet konkrétní potomky – třídy kolekcí pro jednotlivé business-objekty, např. OrderCollection, SubjectCollection, UserCollection, atp.
Jak tedy na to? Nejprve si zavedeme základní třídy, které budu v dalších příkladech používat:
public abstract class BusinessObjectBase { public abstract void Save(); } public class Order : BusinessObjectBase { public decimal Cena { get; set; } public override void Save() { // implementace } }
BusinessObjectBase je bázová třída pro všechny business-objekty, Order je příklad konkrétní implementace.
Způsob 1. – základní
Nejprve si ukážeme, jak vypadá běžná implementace kolekcí:
public abstract class BusinessObjectCollection<T> : Collection<T> where T : BusinessObjectBase { public BusinessObjectCollection() : base(new List<T>()) { } public void SaveAll() { foreach (BusinessObjectBase item in this.Items) { item.Save(); } } public virtual List<T> FindAll(Predicate<T> match) { List<T> innerList = (List<T>)Items; List<T> found = innerList.FindAll(match); return found; } } public class OrderCollection : BusinessObjectCollection<Order> { public decimal GetCelkovaCena() { decimal result = 0; // implementace return result; } }
Co tato implementace ukazuje:
- Jako úplný základ používáme generickou kolekci Collection<T>, která samotná už nám přináší všechny základní strong-type operace, např. Add(), Contains(),IndexOf(), Insert(), Remove(), RemoveAt() a indexer přes pořadí this[int].
- Třída Collection<T> je implementačně pouze wrapperem nějaké vnitřní kolekce, kterou skrývá pod property protected IList<T> Items. Vnitřní implementaceCollection<T> používá jako tento datový nosič List<T>, v kontraktu této třídy to však popsáno není. Chceme-li tedy zaručit, že naše prvky budou fyzicky uloženy v datové struktuře typu List<T>, můžeme využít protected constructor odCollection<T>, který umožňuje předat instanci požadované datové struktury, která musí implementovat IList<T>. Nám se hodí právě List<T>, který zahrnuje spoustu dalších užitečných metod, které budeme chtít naše business-kolekce také naučit. V příkladu využíváme List<T>.FindAll().
- Metoda SaveAll() provádí určitou operaci s prvky kolekce, její interface je vyhovující, není co namítat.
- Metoda FindAll() ale už není tak krásná, jejím návratovým typem je totiž List<T> a tím se nám klientský kód pěkně zamotá:
OrderCollection orders = Order.GetAll(); List<Order> zeroOrders = orders.FindAll(delegate(Order item) { return (item.Cena == 0); }); zeroOrders.SaveAll(); //nejde decimal d = zeroOrder.GetCelkovaCena(); //nejde OrderCollection zeroOrders2 = (OrderCollection)zeroOrders; // nejde
Návratovým typem FindAll() je List<Order>, s kterým přicházíme o veškerou logiku, kterou jsme business-kolekce naučili, jak v bázové třídě BusinessObjectCollection, tak v konkrétní tříděOrderCollection.
Způsob 2. – Drobné vylepšení základní metody
Zlepšení na poli typů dosáhneme, pokud implementaci naší BusinessObjectCollection<T> upravíme takto:
public abstract class BusinessObjectCollection<T> : Collection<T> where T : BusinessObjectBase { public BusinessObjectCollection() : base(new List<T>()) { } public void AddRange(IEnumerable<T> source) { List<T> innerList = (List<T>)Items; innerList.AddRange(source); } public virtual BusinessObjectCollection<T> FindAll(Predicate<T> match) { List<T> innerList = (List<T>)Items; List<T> found = innerList.FindAll(match); BusinessObjectCollection<T> result = new BusinessObjectCollection<T>(); result.AddRange(found); return result; } public void SaveAll() { foreach (BusinessObjectBase item in this.Items) { item.Save(); } } }
- Metodu FindAll() jsme trošku typově vylepšili, namísto obecného List<T> ji necháme vracet alespoň datový typ BusinessObjectCollection<T>. Protože však musíme instanci BusinessObjectCollection<T> nějak získat, nezbývá než si založit novou a pomocí další vypropagované metody List<T>.AddRange() do ní prvky zkopírovat. Je to nepochybně určitý výkonový overhead, nicméně vzhledem k tomu, že kopírujeme pouze reference na existující instance jednotlivých business-objektů, jedná se o overhead zanedbatelný a naopak převáží výhoda příjemného vylepšení interface kolekcí.
- Stále však nemůžeme být spokojeni:
OrderCollection orders = Order.GetAll(); BusinessObjectCollection<Order> zeroOrders = orders.FindAll(delegate(Order item) { return (item.Cena == 0); }); zeroOrders.SaveAll(); //HURÁ! Funguje! decimal d = zeroOrders.GetCelkovaCena(); // stále nefunguje OrderCollection zeroOrders2 = (OrderCollection)zeroOrders; // nejde
Stále nedostáváme od metody FindAll() kolekci typu OrderCollection. Sice jsme získali možnost využít operací implementovaných v BusinessObjectCollection, jako např. SaveAll(), stále však nedosáhneme na operace implementované v OrderCollection, nemáme GetCelkovaCena().
Způsob 3. – Kouzlíme s generiky
Generika v .NET Frameworku jsou udělána dobře a snesou všechno. Opakovaně jsem sám u generik předpokládal, že „tohle už přeci nemůže jít“. Generika opravdu unesou hodně:
public abstract class BusinessObjectCollection<TItem, TCollection> : Collection<TItem> where TItem : BusinessObjectBase where TCollection : BusinessObjectCollection<TItem, TCollection>, new() { public BusinessObjectCollection() : base(new List<TItem>()) { } public void AddRange(IEnumerable<TItem> source) { List<TItem> innerList = (List<TItem>)Items; innerList.AddRange(source); } public virtual TCollection FindAll(Predicate<TItem> match) { List<TItem> innerList = (List<TItem>)Items; List<TItem> found = innerList.FindAll(match); TCollection result = new TCollection(); result.AddRange(found); return result; } public void SaveAll() { foreach (BusinessObjectBase item in this.Items) { item.Save(); } } } public class OrderCollection : BusinessObjectCollection<Order, OrderCollection> { public decimal GetCelkovaCena() { decimal result = 0; // implementace return result; } }
Co jsme to udělali?
- Definici třídy BusinessObjectCollection jsme rozšířili o další generický typ naBusinessObjectCollection<TItem, TCollection>, kde TItem je typ prvků kolekce aTCollection je typ kolekce samotné!
- Pomocí generického typu TCollection můžeme nyní postavit metodu FindAll tak, že jejím návratovým typem bude TCollection, tedy požadovaný typ kolekce.
- Získáváme plně typově konzistentní kolekci OrderCollection, jejíž všechny metody používají typ Order jako typ prvku a typ OrderCollection jako typ kolekce!
- Dostáváme jednoduchý snadno použitelný interface:
OrderCollection orders = Order.GetAll(); OrderCollection zeroOrders = orders.FindAll(delegate(Order item) { return (item.Cena == 0); }); zeroOrders.SaveAll(); //HURÁ! Funguje! decimal d = zeroOrders.GetCelkovaCena(); //HURÁ! Funguje! // přetypovávat už ani nepotřebujeme
Způsob 4. – A jak je to u nás?
Generika snesou hodně, opravdu hodně. U nás je to takto:
- V našich class-libraries (HAVIT Framework Extensions) máme abstraktní předkyBusinessObjectBase a BusinessObjectCollection<TItem, TCollection>. Tyto třídy definují operace společné pro všechny business-objekty, na všech našich projektech, prostě úplný základ, jádro.
- Na konkrétních projektech pak používáme generování základního kódu business-tříd, tedy business-objektů i business-kolekcí, na základě vstupního schematu (např. DB diagramu). Generovaný kód umísťujeme do bázových tříd OrderBase aOrderCollectionBase, zatímco vlastní kód zapisujeme až do potomků těchto třídOrder a OrderCollection.
- Při prvním spuštění generátor vygeneruje třídy všechny (třídy Order aOrderCollection vygeneruje prázdné), zatímco při dalších spuštěních generátor přepisuje třídy OrderBase a OrderCollectionBase (náš „dopsaný“ kód tak zůstává nedotčen).
- Výsledný kód tříd vypadá nějak takto:
Nejprve business-objekt Order:
public abstract class OrderBase : BusinessObjectBase { // toto je generovaná třída, generátor ji vždy přepíše, nic se sem nesmí ručně dopisovat public decimal Cena { get; set; } public override void Save() { // implementace } } public class Order : OrderBase { // toto je používaná třída, sem mohu zapsat vlastní kód public decimal GetCenaPoSleve() { // implementace } }
Dále business-kolekce OrderCollection:
public abstract class OrderCollectionBase : BusinessObjectCollection<Order, OrderCollection> { // toto je generovaná třída, generátor ji vždy přepíše, nic se sem nesmí ručně dopisovat } public class OrderCollection : OrderCollectionBase { // toto je používaná třída, sem mohu zapsat vlastní kód public decimal GetCelkovaCena() { decimal result = 0; // implementace return result; } }
Ano! Generika umožňují dokonce v předkovi OrderCollectionBase používat jako TCollection odkaz na vlastního potomka OrderCollection.