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.