HAVIT Knowledge Base

Vývoj webových aplikací, .NET, SQL, návrh
Welcome to HAVIT Knowledge Base Sign in | Join | Help
-
Home Články Forums Obrázky Soubory

Patterns & practices

Architecture patterns, design patterns, best practices, implementace interfaces, ...

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í implementace Collection<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 od Collection<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 na BusinessObjectCollection<TItem, TCollection>, kde TItem je typ prvků kolekce a TCollection 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ředky BusinessObjectBase 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 a OrderCollectionBase, zatímco vlastní kód zapisujeme až do potomků těchto tříd Order a OrderCollection.
  • Při prvním spuštění generátor vygeneruje třídy všechny (třídy Order a OrderCollection 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í

 

 

Published 12. září 2007 9:01 by Robert Haken

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

JNF said:

Super článek, zvlášť s posláním sebe sama do předka. :-)

Nicméně mám dotaz. Je ňáký důvod proč Order dědíte z OrderBase? Proč jste nepožili partial třídy?

listopadu 9, 2007 17:02
 

Robert Haken said:

to JNF: Důvod pro OrderBase namísto partial tříd je takový, že v případě dědičnosti lze kód generovaný do OrderBase overridovat, zatímco v případě partial tříd tato šance chybí a možnost modifikace by byla omezená.

BTW: Používáme i generovnou partial část, např. proto, abychom vygenerovali výchozí constructory s parametry.

listopadu 9, 2007 17:17
 

Karl said:

Pěkné, díky za takové články, jen bych upozornil na drobnost v tom posledním kódu by OrderCollection neměla dědit z OrderBase ale z OrderCollectionBase.

Ještě jednou díky za článek.

listopadu 9, 2007 21:38
 

Robert Haken said:

to Karl: Děkuji za upozornění. OrderBase samozřejmě v tomto článku vůbec není, mělo to být OrderCollectionBase.

listopadu 11, 2007 22:45
 

Rudidlo said:

Jakým způsobem pak vytvořím instanci třídy BusinessObjectCollection ?

května 5, 2008 12:46
 

Robert Haken said:

to Rudidlo: třída BusinessObjectCollection není určena k vytváření instancí, nýbrž slouží pouze jako předek pro specifické kolekce. Správně návodná otázka, třída má být abstraktní...

května 5, 2008 20:08
 

Rudidlo said:

to Robert Haken: díky za vysvetleni - me to netrklo.

května 6, 2008 9:57

What do you think?

(required) 
(optional)
(required) 
Enter the code you see below

Submit