Tag Archives: Business Layer

Money – kouzlíme s generiky II.

Generika jsou dobrá nejenom pro kolekce. Že se dají velmi dobře využít i v jiných situacích, dnes předvedu na třídě Money  (diskuze, kdy má být Money třída a kdy je lepší struct není předmětem tohoto článku). Uvidíte, že jdou dělat i takové věci jako generické operátory.

Zadání problému

Chceme řešit imlementaci třídy Money, třídy reprezentující peněžní datový typ skládající se z částky (Amount) a její měny (Currency). Naším cílem je dosáhnout následujího:

  • chceme zavést opakovaně využitelnou třídu Money (typicky do vlastní class-library) – ta má obsahovat vlastnost Amount (částku typu decimal) a vlastnost Currency (měnu), jejíž typ obecně naznáme a chceme ho ponechat na libovůli klientské aplikace (známe však pravidla pro obecné Money, které nám říkají, že bez kurzových převodů můžeme běžné operace provádět jen mezi operandy stejné měny – porovnávání, sčítání/odečítání, násobení/dělení číslem, dělení mezi sebou, atp.). Do class-library tedy chceme implementovat třídu Money<TCurrency>.
  • implementaci veškeré logiky chceme mít součástí této třídy Money<TCurrency> – v této třídě chceme definovat potřebné operace a centrálně je udržovat v class-library, chceme mít pokryté všechny základní operátory, atp. atp.
  • v jednoduchých svých projektech zadefinujeme třídu Currency (obvykle business-object odpovídající číselníku měn v DB s vlastnostmi Nazev, Zkratka, atp.) a budeme v nich třídu Money<TCurrency> používat přímo v kódu
Money<Currency> cena = new Money(150.20M, Currency.Czk);
cena = cena + new Money(20M, Currency.Czk);
cena = cena * 10;
MyLabel.Text = cena.Amount.ToString("n2") + " " + cena.Currency.Zkratka;
...
  • v rozsáhlejších našich projektech máme také třídu Currency, chceme však vytvořit odvozenou negenerickou třídu Money, která bude základní funkčnost obecnějšího Money<TCurrency> rozšiřovat o nějaké nové project-specific možnosti, např. ony kurzové přepočty, standardní výstup, atp. Použití zamýšlíme nějak takto:
Money cena = new Money(10M, Currency.Czk);
cena = cena.ToCurrency(Currency.Usd) + new Money(159.3M, Currency.Usd);
MyLabel.Text = cena.ToString("n2");
...
  • stále však trváme na tom, že implementace běžných operací (zejména operátorů) musí být použita z obecného Money<TCurrency>. Uvědomme si však základní problém – jak se vyhnout nové definici operátorů v negenerickém potomkovi Money : Money<Currency>, když operátory jsou statické, nejsou předmětem dědičnosti a v Money<TCurrency> budou vypadat nejspíš nějak takto:
public static Money<TCurrency> operator +(Money<TCurrency> money1, Money<TCurrency> money2)
{
// implementace
}

…v potomkovi Money chceme přeci součet dvou Money a hlavně návratový typ Money, nikoliv Money<Currency>

public static Money operator +(Money money1, Money money2)
{
// znovu implementace??? Tomu jsme se chtěli vyhnout! Implementaci chceme mít v class-library.
}
Kouzlo první – generická implementace funkčnosti operátorů

Čeho chceme dosáhnout – mít centralizovanou implementaci funkčnosti operátorů tak, abychom ji mohli snadno udržovat pro všechny naše projekty. Kouzlo první spočívá v extrahování implementace operací do generických metod třídy Money<TCurrency>, kteréžto metody sama třída Money<TCurrency> ve svých operátorech využije, stejnětak jako je může využít libovolný potomek při definici svých typovaných operátorů.

Money<TCurrency>:

public static Money<TCurrency> operator +(Money<TCurrency> money1, Money<TCurrency> money2)
{
    return SectiMoney<Money<TCurrency>>(money1, money2);
}

public static TResult SectiMoney<TResult>(Money<TCurrency> money1, Money<TCurrency> money2)
    where TResult : Money<TCurrency>, new()
{
    if (money1.Currency != money2.Currency)
    {
        throw ...
    }

    TResult result = new TResult();
    result.Amount = money1.Amount + money2.Amount;
    result.Currency = money1.Currency;
    return result;
}

Obdobně jako u BusinessObjectCollection využíváme kouzla generika s „cílovým typem“. Daní za tento způsob je nám nutnost vyžadovat constructor bez parametrů, abychom v generiku byly schopni vytvořit instanci cílového typu (možnost generika s constructorem předepsaných parametrů se asi v dohledné době nedočkáme, v .NET 3.5 pokud vím, nic takového stále není a po diskuzi s částí CLR teamu na letošním MVP Summitu v Redmondu už i chápu proč).

Project-specific potomek Money už pak může využívat této centrální implementace snadno:

public static Money operator +(Money money1, Money money2)
{
    return SectiMoney<Money>(money1, money2);
}
Kouzlo druhé – projektový potomek Money bez nové implementace operátorů

Řekněme, že se chceme v projektech, v project-specific třídách Money, úplně zbavit nutnosti implementovat operátory, byť už máme cestu jak v těchto operátorech využít centrální implementace funkčnosti. Projektový kód chceme mít co nejčistší, prostý takovýchto infrastrukturních redefinic, tak, abychom se v něm mohli přehledně věnovat jen nové funkčnosti Money oproti předkovi Money<TCurrency> – např. doplnit ony omílané kurzové přepočty, atp.

Jak na to?

Do řetězce dědičnosti mezi Money a Money<TCurrency> vložíme kouzelnou třídu MoneyImplementationBase, která nás dalším fíglem s generiky a operátory posune ke kýženému výsledku…

public abstract class MoneyImplementationBase<TCurrency, TResult>: Money<TCurrency>
    where TCurrency: class
    where TResult : MoneyImplementationBase<TCurrency, TResult>, new()
{
    public static TResult operator +(MoneyImplementationBase<TCurrency, TResult> money1, MoneyImplementationBase<TCurrency, TResult> money2)
    {
        return SectiMoney<TResult>(money1, money2);
    }
}

Tato třída samozřejmě patří do naší centrální class-library odkud ji budou využívat jednotlivé projekty.

A výsledek? Plně funkční projektové Money pak dostaneme takto:

public class Money : MoneyImplementationBase<Currency, Money>
{
    // toť vše!
    // sem už můžeme doplnit jen project-specific rozšíření Money, veškerou základní funkčnost nám poskytují předci
}

Hezké, že? Stejně jako u kouzlení s BusinessObjectCollection předhazujeme generickému předkovi sami sebe (pokud to bude potřeba, můžeme předhodit i svého potomka).

BusinessObjectCollection – kouzlíme s generiky

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:

  1. 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ů.
  2. 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.
  3. 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.
  4. 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.

Pokračování