\


 Sunday, 09 April 2006
Bázová třída pro typové kolekce v .NET Frameworku 2.0

V souvislosti s uvedením generiky v .NET Frameworku se v různých článcích dočtete, jak generika usnadní vytváření a použití typových kolekcí. To je sice pravda, ale v článku se kromě zjednodušených příkladů a frikulínských hlášek o dokonalosti .NETu 2.0 z úst (respektiva pera) excitovaných jedinců po právě dokonaném intimním styku se softwarovou emanací božstva Microsoftu :-D málokdy dovíte, jak by taková typová kolekce měla vypadat v běžné aplikaci.

Proč nevyhovuje obyčejné použití generického typu List? (deklarace typové kolekce objednávek ve tvaru List<Order> orderCollection = new List<Order>()).

  • Jednou z dobrých praktik u generik je co nejvíce před uživateli (dalšími vývojáři) skrývat informaci, že pracují s generickým typem. Ač mně syntaxe pro práci s generickými typy připadá intuitivní, nemusí si to myslet všichni, a mnoho vývojářů stále asi raději používá důvěrně známý kód OrderCollection orderCollection = new OrderCollection() místo výše zmíněného kódu List<Order> orderCollection = new List<Order>()).  Tento požadavek by ale List<T> splnil  - typová kolekce může být potomkem List<T>.
  • Ve třídě List nejsou metody Add a Remove virtuální. To znamená, že nemůžete po přidání nebo odebrání položky z/do kolekce vyvolávat vlastní události. A to je problém, protože po přidání/odebrání položek z kolekce můžeme chtít nastavit/zrušit rodiče nebo přepočítát sumární hodnoty za položky v kolekci apod.

Bázová třída pro všechny kolekce, kterou používám, je otevřeným generickým typem a jejím předkem je třída Collection<T> z jmenného prostoru System.Collections.ObjectModel. Třída Collection<T> nabízí virtuální chráněné metody InsertItem a RemoveItem, ve kterých můžete vyvolávat potřebné události. Jestliže používate návrhový vzor Layer SuperType a máte tedy jednu bázovou třídu pro všechny business objekty (BusinessObjectBase), je vhodné, aby bázová třída pro kolekce kladla na generický typ T omezení, že musí být potomkem třídy BusinessObjectBase. Omezení slouží k tomu, abyste ve svých kolekcích mohli intuitivně používat všechny atributy a metody deklarované na úrovni společného předka BusinessObjectBase.

Kód kolekce:

   public class BusinessCollectionBase<T> : Collection<T> 
                                        where T : BusinessObjectBase
    {
        #region Events
        public event EventHandler<CollectionChangeEventArgs> ItemAdded;
        public event EventHandler<CollectionChangeEventArgs> ItemRemoved;
        #endregion Events
        #region Protected methods
        /// <summary>
        /// Přidání položky do kolekce
        /// </summary>
        /// <param name="index">Index položky</param>
        /// <param name="item">Vkládaná položka</param>
        protected override void InsertItem(int index, T item)
        {
            base.InsertItem(index, item);
            OnItemAdded(new CollectionChangeEventArgs(item));
        }

        /// <summary>
        /// Přidání položky do kolekce
        /// </summary>
        /// <param name="index">Index položky</param>        
        protected override void RemoveItem(int index)
        {
            T item = this[index];
            base.RemoveItem(index);
            OnItemRemoved(new CollectionChangeEventArgs(item));
        }

        /// <summary>
        /// Metoda odpovědná za vyvolání události ItemRemoved
        /// </summary>
        /// <param name="e">Argumenty události</param>
        protected void OnItemRemoved(CollectionChangeEventArgs e)
        {
            if (ItemRemoved != null)
            {
                ItemRemoved(this, e);
            }
            
        }
        
        /// <summary>
        /// Metoda odpovědná za vyvolání události ItemAdded
        /// </summary>
        /// <param name="e"></param>
        protected void OnItemAdded(CollectionChangeEventArgs e)
        {
            if (ItemAdded != null)
            {
                ItemAdded(this, e);
            }
         
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public virtual T FindById(Guid id)
        {
            List<T> mylist = (List<T>) Items;
            T objectMeetsCriteria = null;
            objectMeetsCriteria = mylist.Find(delegate(T iterObject)
                                  {
                                      if (iterObject.Id == id)
                                      {
                                          return true;
                                      }
                                      else
                                      {
                                          return false;
                                      }
                                                                
                                  });

            return objectMeetsCriteria;
        }

        /// <summary>
        /// Nalezení všech objektů splňujících zadanou podmínku
        /// </summary>
        /// <param name="criteria">Podmínka výběru</param>
        /// <returns>List s objekty, které splňují zadanou podmínku</returns
        public virtual List<T> Find(Predicate<T> criteria)
        {
            List<T> mylist = (List<T>) Items;           
            return (mylist.FindAll(criteria));
            
        }
        
        /// <summary>
        /// Spuštění akce nad všemi elementy v kolekci
        /// </summary>
        /// <param name="action">Akce, která se má provést</param>
        public virtual void ForEach(Action<T> action)
        {
            List<T> mylist = (List<T>)Items;
            mylist.ForEach(action);            
        }
    
        #endregion Protected methods
    }
}

Jak vidíme:

  1. Třída BusinessCollectionBase je potomkem třídy Collection<T> a vyžaduje, aby typ T byl vždy potomkem BusinessObjectBase. Motivace pro toto rozhodnutí jspu popsány výše.
  2. Nadeklarovali jsme dvě události ItemAdded a ItemRemoved, které jsou vyvolávany v přepsaných metodách InsertItem (ItemAdded) a RemoveItem (ItemRemoved). Pokud bych měl zájem, mohu jednoduše přidat i události vyvolávané před přidáním/odebráním položky z kolekce.
  3. Do rozhraní BusinessCollectionBase jsem také přidal několik zajímavých metod.
    1. Metoda FindById nalezne podle předaného Id (unikátní identifikátor instance) objekt v kolekci. V této metodě opět používáme nové konstrukce z .Net Frameworku 2.0. Implementační objekt kolekce (starý známý List<T> ) vystavuje metodu Find, která očekává generického delegáta Predicate z jmenného prostoru System.
      public delegate bool Predicate( T obj);
      Delegát Predicate je "ukazatelem" na metodu, která očekává jeden generický argument T a vrací true nebo false. Delegát Predicate tedy zastupuje metodu s podmínkou, která je pro předaný argument obj pravdivá nebo nepravdivá. My pro vytvoření podmínky použijeme anonymní metodu, která vrátí true pouze tehdy, když se Id objektu v kolekci shoduje s předaným Id. Atribut Id u generického typu T kolekce můžeme používat právě proto, že jsme zavedli pro typ T omezení (musíš být potomkem  BusinessObjectBase) a atribut Id je deklarován ve třídě BusinessOBjectBase.
    2. Pro pokročilejší operace s elementy kolekce jsme z objektu List<T> zveřejnili metody FindAll A ForEach. Metoda FindAll podle předané podmínky (delegát Predicate) nalezne a vrátí všechny objekty, které jí vyhovují. Metoda ForEach spustí pro všchny elementy v kolekci "akci - činnost" implementovanou v metodě, na níž "ukazuje" další užitečný delegát Action<T>.

      public delegate void Action<T> ( T obj);

      Když tedy budete chtít všechny objekty v kolekci zrušit, místo psaní cyklu foreach napíšete kód podobný tomuto:

      myCol.ForEach(delegate(OrderItem item)
      
                  {
      
                       item.Discard();
      
                  });
      

Vytváření vlastních typových kolekcí je jednoduché:

    /// <summary>
    /// Kolekce objektů OrderItem
    /// </summary>
    public class OrderItemCollection : BusinessCollectionBase<OrderItem>
    {

    }
 

Pro úplnost sem dávám triviální kód třídy pro argumenty události CollectionChanged.

    /// <summary>
    /// Objekt v kolekci
    /// </summary>
    public class CollectionChangeEventArgs : EventArgs
    {
        #region Private variables
        private BusinessObjectBase m_collectionObject;
        #endregion Private variables
        
        #region Constructors
        /// <summary>
        /// Konstruktor
        /// </summary>
        /// <param name="collectionObjekt">Objekt v kolekci, kterého se událost týká</param>
        public CollectionChangeEventArgs(BusinessObjectBase collectionObject)
        {
            BasicValidations.AssertNotNull(collectionObject, "collectionObject");
            m_collectionObject = collectionObject;
        }

        /// <summary>
        /// Objekt v kolekci, kterého se událost týká
        /// </summary>
        public BusinessObjectBase CollectionObject
        {
            get
            {
                return m_collectionObject;
            }
        }
        #endregion Constructors
    }
Související články:

Bázová třída pro business objekty - návrhový vzor Layer Supertype
Cachování řádků z databáze pro business objekty - třída DataCacheHelper
Ukázka použití třídy BusinessObjectBase



Sunday, 09 April 2006 14:34:33 (Central Europe Standard Time, UTC+01:00)       
Comments [7]  .NET Framework | Compact .Net Framework | Návrhové vzory


Wednesday, 12 April 2006 12:11:48 (Central Europe Standard Time, UTC+01:00)
Zkušení programátoři znají generika i jejich syntaxi z C++ templates, i když princip jejich implementace je trochu jiný.
C# generika sdílejí jeden kód pro všechny referenční typy, C++ generuje nový kód pro každé použití generika s novou třídou a to vede k nárůstu kódu.
Implementace C# totiž využívá teorii typů v programovacích jazycích nad kterou si bádají akademici, což je záležitost která vypadá abstraktně ale má své významné praktické dopady.
A když už jsme u té teorie a computer science, přicházející LINQ je přesně to samé - převedení akademického výzkumu programovacích jazyků do praxe. Akademický výzkum abstraktních struktur a typů teď usnadní práci programátorům na praktických projektech a to je dobře. Já osobně mám radost z toho že "exotické" jazyky které mám rád a jejichž myšlenky mě okouzlují (ML, Haskell) budu za pár let používat v práci ve Visual Studiu, byť v trochu jiné syntaxi...

Honza

P.S.
René, z "frikulínských hlášek z úst excitovaných jedinců" mi zaskočila poobědová káva, to nám nedělej!
Wednesday, 12 April 2006 12:13:29 (Central Europe Standard Time, UTC+01:00)
ještě teď se směju...
Honza Stoklasa
Wednesday, 12 April 2006 21:11:49 (Central Europe Standard Time, UTC+01:00)
Ahoj Honzo,
jsem rad, ze ses pobavil a snad ses u toho moc nepoprskal :)
Jak vubec zijes? Uz jsem o tobe dlouhou dobu nic neslysel...
Friday, 21 April 2006 14:24:08 (Central Europe Standard Time, UTC+01:00)
Nepřijde mi ideální publikovat inner-listovou metodu FindAll() pod jménem Find(). Neměli bychom plést programátorům hlavu tím, že Find() od List<T> vrátí první match, zatímco náš Find() všechny - doporučuji dodržet pattern z .NET Frameworku a udělat Find() i FindAll().

Taky by bylo lepší, aby FindAll() vracel rovnou zase BusinessCollectionBase<T>, stačí využít ten wrappující constructor Collection<T>(IList<T>) a máme to hned.

...ale jinak prima seriál, už se těším na další díl.

Robert
Friday, 21 April 2006 14:30:40 (Central Europe Standard Time, UTC+01:00)
Robert Haken:
Ad FindAll) Mate pravdu, pojmenovavaci konvence vychazi z mych projektu jeste pred existenci NF 2.0. Pojmenovani FindAll je samozrejme lepsi.

Ad navratova hodnota) Ano, i to je namet na vylepseni, i kdyz si nevzpominam, ze bych nekdy po vyhledani zaznamu potreboval neco jineho nez rozhrani List<T>

Diky za pripominky, jsem rad za kazdou vecnou reakci!
Friday, 21 April 2006 14:34:34 (Central Europe Standard Time, UTC+01:00)
Taky mi vrtá hlavou, jestli je rozumné spoléhat na implementaci Items jako List<T> - je to přecijenom zapouzdřeno a i když je to velmi velmi nepravděpodobné, tak by to mohl teoreticky nějaký update frameworku sestřelit.

Naštěstí to jde snadno vyřešit constructorem

public BusinessCollectionBase() : base(new List<T>()) {}
Friday, 21 April 2006 14:42:33 (Central Europe Standard Time, UTC+01:00)
Mate pravdu - takto presne to mam v projektu, chtel jsem to ale zminit teprve pote, co reknu, jake jsou nevyhody vsech stavajich postupu.
Presto - vcera i dneska jsme mel velmi trefne poznamky, tleskam ;)
Comments are closed.