Použití generických typů v Design Patterns

autor RNDr. Ilja Kraval

únor 2008

www.objects.cz

 

 

 

 

 

Co jsou to generické typy?

 

V objektovém programování se můžeme často setkat s následující situací:

Napíšeme určitou část kódu a zjistíme, že tato část kódu by se hodila opětovně použít i jinde, protože se jedná o (takřka) stejný kód, který se liší v novém použití jen a pouze tím, že v některých místech používáme proměnné jiného typu, než jsme jaké jsme použili v kódu prvním. Například máme napsaný kód třídy s nějakou metodou a tato metoda má vstupní parametr typu string. Následně zjišťujeme, že tentýž navlas shodný kód bychom potřebovali použít znovu, jenom s tím rozdílem, že metoda pracuje namísto s proměnnou typu string s proměnnou typu integer.

Narazili jsme na něco jako „typový problém“ v opětovné použitelnosti. Jak taková na první pohled zvláštní situace „typového problému re-use“ může vzniknout?

Jako klasický příklad se v učebnicích uvádí program pro tzv. zásobník, tj. skupinu objektů, které jsou sekvenčně řazeny do řady a poté opět vyjímány z této řady ve stejné sekvenci zpět. Takový obecný zásobník zavedeme jako třídu a vybavíme ji metodami push() a pop(). První z nich push()vkládá do zásobníku prvek, druhá pop()jej vybírá metodou FIFO „first in, first out“. K „typovému problému v re-use“ dojdeme u zásobníku takto:

Nejprve bychom napsali kód pro zásobník s prvky typu string a tedy s metodami push(string inObj) a pop() s návratovým typem string. Ve druhém případě u prvků typu integer bychom napsali kód pro zásobník s prvky typu integer s metodami push(integer inObj) a integer pop(). Asi nás nepřekvapí zjištění, že oba kódy jsou opravdu shodné až na jednu drobnost: V prvním kódu se používají prvky jednoho typu (string)a v druhém prvky druhého typu (integer) a co je důležité, ničím jiným se tyto dva kódy od sebe neliší!

Existuje jedno klasické řešení, jak dát oba kódy pod jednu střechu, ale při možnosti použití generických typů není tento postup zásadně doporučován: Napíšeme  kód po obecný „hluchý“ typ (tj. pro typ v nejvyšší v hierarchii tříd daného jazyka, např. v C# je to Object) a poté budeme ručně přetypovávat vstupy a výstupy na náš požadovaný typ: Například u zásobníku vytvořme nejprve obecný zásobník (třídu nazvěme například CStock), tento zásobník bude pracovat s prvky typu Object (tj. nejvyšší typ). Dále pro každý požadovaný typ vytvoříme novou třídu. Objekty z těchto tříd obsahují vytvořený obecný zásobník typu CStock a pouze přetypovávají vstupní a výstupní parametry na požadovaný typ. Například v pseudojazyce OOP by metoda pop pro třídu CStock_string vypadala nějak takto:

 

class CStock_string

{

...

private CStock mStock = new CStock();

...

public string pop()

 {

  return (string)mStock.pop();    

 }

...

}

(pozn. závorka a v ní typ je zde chápán jako přetypování na daný typ, zde na string)

Pokud máme možnost použít generické typy, tak je doporučováno se tomuto „ručnímu přetypování“ určitě vyhnout. Mechanismus generických typu totiž umožňuje mnohem vyšší úroveň typové kontroly, což je pro  stability aplikací vyžadováno, a  současně při použití mechanismu generických typů získáme mnohem vyšší úroveň opětovné použitelnost. Jinak řečeno to znamená, že použitím generických typů se kód méně opakuje, a tedy míň se při psaní kódu nadřeme, a to hlavně při změnách kódu (!).

 

 

Jak funguje mechanismus generických typů neboli typoví žolíci

 

Použití generických typů je z hlediska syntaxe poměrně dost jednoduché.  Pokud zavádíme novou generickou třídu, tak za název třídy umístíme závorky ve tvaru <>. e V kódu této třídy se výčet uvedený v této v závorce chápe jako výčet parametrů, kde každý parametr značí parametr  typu. Laicky řečeno, to, co se vyskytuje ve výčtu v závorce <>, tak to se následně v kódu třídy objeví v pozici typu (a nikde jinde) a je to volný parametr, například takto:

 

class CStock<MujParametrTypu> 

{

 

public MujParametrTypu pop()

 {

 ...

 }

public void push (MujParametrTypu InObj)

 {

 ...

 }

...

}

 

Tento kód jako příklad je pak připraven pro použití s jedním volným parametrem typu, který se v tomto případě nazývá MujParametrTypu a v budoucnu bude nahrazen buď typem string, nebo integer nebo jiným.

 

 

Poznámka: V daném jazyce je třeba si prostudovat v dokumentaci i určitá specifika, například další syntaxi nutnou pro případ, kdy se mají objekty z parametrických typů rodit aj. Zde uvádíme základní princip fungování generických typů

 

 

Při použití této třídy musíme současně povinně specifikovat a dosadit za volné parametry konkrétní typy, což provedeme tak, že do pozic ve výčtu dosadíme v kódu konkrétní typy, například takto:

 

...

private CSstock<string> myStrStock;

...

 

Pro jednoduchost si můžeme představit, že kompilátor vezme námi zavedený konkrétní typ (zde string), vezme třídu, zde CStock<MujParametrTypu> s parametrem MujParametrTypu, a vytvoří kompilací novou třídu tak, že všude za parametr MujParametrTypu dosadí v kompilovaném kódu typ string. Tedy aniž bychom psali nový kód, tak vznikla nová třída konkretizací parametru typu!

 

 

Poznámka: Některá prostředí však nemusí generovat novou třídu (např. Java) a třídu s parametrem chápou pouze jako jednu třídu pro všechny možné dosazené typy, tj. pouze zkontrolují správně dosazenou typovost a vše funguje jakoby s jednou jedinou třídou. Na tuto skutečnost musíme být opatrní při použití statických členů. V dynamických instancích se tato skutečnost neprojeví. Oproti Javě naopak C# umožňuje použití statických členů u generických tříd. Opět připomenu, že je třeba prostudovat help daného prostředí, zde soustřeďujeme na základy fungování. 

 

 

Je vidět, že generické typy fungují podobně jako žolíci ve známé karetní hře. Za ně se dosadí konkrétní požadovaný typ a kód se tak může opětovně použít pro různé typy.

 

Třídy s generickými typy dodané z prostředí jazyka

 

Zavedení generických typů umožňuje poskytovatelům frameworku daného prostředí (např. Java, C# ) nabídnout větší luxus pro tvůrce programu. Například celá knihovna pro práci se seznamy objektů může být postavena na tomto principu: Zatímco zastaralejší způsob práce s kolekcemi využívá pouze typ Object (nejvyšší typ) a je tedy nutné vždy ruční přetypování, tak kolekce  s generickými typy umožňují vytvořit přímo  kolekce s volnými typy a následně použít „typy šité na míru“.

Jako příklad si můžeme uvést rozdíl mezi třídou Hashtable a Dictionary v C# (podobně to samozřejmě funguje i v jazyku Java): Třída Dictionary je zobecněním třídy Hashtable pomocí generických typů. „Starší negenerická “ třída Hashtable zavádí možnost pracovat s kolekcemi objektů i s tzv. klíčem, tj., do seznamu umísťujeme dvojici objekt + klíč. K tomu slouží metoda Add:

 

public virtual void Add (

   Object key,

   Object value

)

Jak vidět, vstupní parametry jsou dva a jsou typu Objekt, tj. za tyto prvky může být dosazen libovolný objekt. Tímto však z našeho typového objektu uděláme totálně hluchý (tj. netypový) objekt. Nejenže může nastat problém v run-time s chybou přetypování, ale pro každý nový případ použití kolekce musíme zavádět novou třídu s daným přetypováním.

Pokud použijeme generickou třídu Dictionary, situace se výrazně změní k lepšímu. Generická třída Dictionary obsahuje dva parametry typu, a to TKey a TValue. Její definice odpovídá tomuto zápisu:

 

public class Dictionary<TKey,TValue>:...

...  

 

a metoda Add má logicky tvar tento:

 

public void Add (

   TKey key,

   TValue value

   )

 

Pokud chceme tuto třídu použít, musíme současně s jejím použitím specifikovat oba volné typy a to typ klíče a typ  vstupujícího objektu (tj. TKey a TValue), musíme tedy dosadit za tyto typy konkrétní typy, například takto:

 

...

private Dictionary<string, MyClass> mDict = new Dictionary<string, MyClass>();

...

Tím jsme automaticky zavedli obdobu původního Hashtable s konkrétními typy: typ klíče je string, typ objektu MyClass. Po inicializaci objektu pak můžeme volat metodu Add se vstupními parametry, které musí být povinně typu  string a myClass, například takto:

mDict.Add(myBarva.kod, myBarva);

kde kód je property typu string

 

Použití generických typů pro návrhový vzor (Design Pattern) STRATEGY

 

Protože se generická třída chová jako obdoba šablony s volnými parametry typů, je možné použít konstrukci generických typů pro některého ze známých návrhových vzorů. V další části článku si ukážeme použití tohoto přístupu pro jednoduchou a efektivní realizaci vzoru STRATEGY.  

Jak známo, hlavní myšlenka vzoru STRATEGY (viz například ekniha „Design Patterns v OOP“, zdarma na našem webu) spočívá v možnosti zavést množinu vyměnitelných algoritmů, což reprezentuje množina objektů téže rodiny s polymorfní metodou algoritmu. Objekty se umístí do kolekce s klíčem (viz například Dictionary v předešlé kapitole), následně lze pomocí klíče vybírat algoritmus: Volbou klíče se vybere objekt z kolekce, tedy algoritmus, a spustí se polymorfní metoda tohoto objetu. Jiný klíč, jiný objekt, jiný algoritmus...   

Konstrukce tohoto vzoru pomocí generických typů může být například následující:

Nejprve zavedeme šablonu pro dané strategie pomocí interfacu, nazvěme jej například IStrategy:

 

public interface IStrategy<TParIn, TParOut>

    {

        TParOut Do(TParIn aInpar);

    }

 

Daný interface  má jedinou metodu k přepsání a tou je metoda Do. Zde se právě využije mechanismu generických typů pro libovolné vstupní nebo výstupní parametry. Vstupní i výstupní parametr mají „volné typy“, které se specifikují až při implementaci vzoru STRATEGY.

Další částí vzoru je tzv. Strategy Manager, tj. ona zmíněná kolekce s klíčem. Tato kolekce bude držet sadu objektů, které podporují uvedený interface IStrategy. Tohoto držitele strategií zavedeme jako objekt obalující objekt z generické třídy Dictionary  (o Dictionary viz předešlá kapitola):

 

public class StrategyManager<TParIn, TParOut, TParKey>

  {

  private Dictionary<TParKey, IStrategy<TParIn, TParOut>> mDict = new Dictionary<TParKey,IStrategy<TParIn,TParOut>>();

       

  public void RegisterStrategy(TParKey aKey, IStrategy<TParIn, TParOut> aStrat)

        {

            mDict.Add(aKey, aStrat);

        }

 

  public TParOut DoStrategyByKey(TParKey aKey, TParIn aIn)

        {

            IStrategy<TParIn, TParOut> lStrat = (IStrategy<TParIn, TParOut>)mDict[aKey];

            return lStrat.Do(aIn);

 

        }

    }

 

Všimněme si podrobněji tohoto kódu.

Zavádíme generickou třídu StrategyManager se třemi parametry typu: TParIn, TParOut a TParKey. Úmyslně a pro přehlednost jsou první dva parametry nazvány stejně, jako byly nazvány parametry při zavedení interfacu IStrategy. Důvod je ten, že oba parametry kódu této třídy vystupují ve stejném významu, jako u interfacu IStrategy.

Dále je ve třídě zaveden jeden služební dynamický člen (member) s názvem mDict, který vnitřně spravuje kolekci objektů. Tento člen pochází z generické třídy Dictionary s již specifikovanými typy. I když to tak na první pohled nevypadá, tak v řádku kódu:

private Dictionary<TParKey, IStrategy<TParIn, TParOut>> mDict...//etc. 

má použitá generická třída Dictionary již vyplněny oba „volné typy“, tj. TKey a TValue. Do prvního TKey je dosazen typ TParKey a do druhého TValue je dosazen IStrategy<TParIn, TParOut>.

Dále jsou v třídě zavedeny dvě metody. První slouží pro registraci algoritmu, což není nic jiného, než delegování na Add vnitřní kolekce.

Druhá metoda DoStrategyByKey provádí výběr algoritmu podle klíče a současně jej spustí. Vstupními parametry musí být jak klíč, tak vstupní parametr algoritmu, výstupním parametrem musí být výstupní parametr algoritmu. 

Nyní si představme, že tento kód jako šablona je připraven k použití v knihovně v modulu, který se nazývá například PatternStrategy. Programátor, který chce tento vzor použít, musí definovat jednotlivé strategie, tj. implementovat interface IStrategy, a definovat typy, tj. vyplnit typy v šabloně. Tímto vzor „istanciuje“ pro své použití.

 

Jako testovací příklad můžeme zvolit tyto dva primitivní algoritmy

 

public class MyStrategy1 : IStrategy<string, string>

    {

        string IStrategy<string, string>.Do(string aInpar)

        {

            return "kuk1";

 

        }

 

public class MyStrategy2 : IStrategy<string, string>

    {

        string IStrategy<string, string>.Do(string aInpar)

        {

            return "kuk2";

 

        }

A zavedeme náš vlastní StrategyManager takto:

 

StrategyManager<string, string, int> myStratManager = new StrategyManager<string,string,int>();

 

Odzkoušení funkčnosti kódu si pak můžeme provést natvrdo v kódu (například kód při kliknutí na tlačítko):

 

StrategyManager<string, string, int> myStratManager = new StrategyManager<string,string,int>();

MyStrategy1 strat1 = new MyStrategy1();

myStratManager.RegisterStrategy(1, strat1);

 

MyStrategy2 strat2 = new MyStrategy2();

myStratManager.RegisterStrategy(2, strat2);

 

MessageBox.Show(myStratManager.DoStrategyByKey(1, ""));

MessageBox.Show(myStratManager.DoStrategyByKey(2, ""));

 

Závěr

 

Mechanismus generických typů (resp. šablon v C++) lze použít pro zavedení šablon vzorů, například vzoru STRATEGY.

Výhoda použití takové šablony vzoru spočívá v jednotnosti použití a to až přímo v kódu. Uživatel, tj.  programátor, dostává šablonu z knihovny napsanou přímo v kódu s přesně definovaným postupy užití:

 

1.    Musí přepsat metody (implementovat interface)  

2.    Současně musí vyplnit volné typy za své konkrétní typy

 

Kromě výhody jednotnosti postupu v kódu je také nemalou výhodou i zpětná čitelnost jednotně napsaného kódu pomocí vzoru, protože generické třídy si ponechávají svůj název, zde například  třída StrategyManager.

 

vyplňte prosím anketu k článku, děkuji.

 

 

konec článku