Nahoru
 

Návrhový vzor Iterátor

Návrhové vzory popisují vhodná řešení častých programátorských problémů. Jedním takovým je i problém s procházením obecné struktury, jež obsahuje nějaké seskupené prvky (pole, spojový seznam, binární strom apod.). Pro tuto úlohu nachází řešení návrhový vzor iterátor.

Hlavním využitím iterátoru je přístup k obsahu objektu, který seskupuje prvky, bez odhalení jeho vnitřní struktury. Zkrátka chceme se strukturou pracovat pomocí jednoduchého rozhraní tak, abychom nemuseli řešit její konkrétní implementaci. Typicky iterátor poskytuje následující metody:

  • void First() – vrácení iterátoru do výchozí hodnoty
  • void Next() – posunutí iterátoru na další prvek
  • bool IsDone() – kontrola toho, zda iterátor už prošel celou strukturou
  • T CurrentItem() – současná hodnota, na kterou iterátor ukazuje

Další využití iterátoru nalezneme v možnostech různých průchodů seskupených prvků. Různé konkrétní iterátory implementující jednotné rozhraní mohou procházet jednotlivé prvky jakýmkoliv způsobem. Kdybychom zvolili jako naši strukturu binární strom, mohli bychom mít různé iterátory pro pre-order, in-order a post-order průchody se zachováním stejného rozhraní.

Příklad

Pro lepší pochopení tématu zde uvedu jednoduchý příklad iterátorů nad obyčejným polem. V reálném světě se s největší pravděpodobností s iterátory nad poli nesetkáme, avšak pro znázornění funkčnosti iterátorů to bude dobré. Celý projekt je psán v jazyce C# a můžete si jej prohlédnout v repozitáři na GitHub.

Prvně definujeme rozhraní pro iterátor se všemi jeho metodami, které budeme vyžadovat od konkrétních implementací iterátorů.

// Zde definujeme rozhraní pro náš iterátor
interface IIterator<T>
{
    void First(); // reset iterátoru, přesun na první pozici
    void Next(); // posunutí o prvek vpřed
    bool IsDone(); // kontrola konce průchodu
    T CurrentItem(); // získání prvku, na který iterátor právě "ukazuje"
}

Iterátor může obsahovat více metod, které rozšíří jeho funkčnost. Zde pro jednoduchost uvádím pouze nejnutnější základ pro jeho chod (pravděpodobně bychom se dokázali obejít i bez metody First() pokud by nebyl potřeba reset iterátoru do původní polohy).

V dalším kroku vytvoříme konkrétní implementace iterátorů. První iterátor bude procházet prvky v pořadí, v jakém jsou v poli uloženy. Druhý iterátor se od prvního bude lišit pouze v tom, že prvky „projede“ pozpátku.

// Konkrétní implementace iterátoru pro pole
class ArrayIterator<T> : IIterator<T>
{
    private T[] _array;
    private int _currentIndex;

    // V konstruktoru si předáme pole, jež budeme procházet iterátorem
    public ArrayIterator(T[] array)
    {
        // provedeme ošetření pro prázdné pole
        if (array == null)
            throw new ArgumentException("Array cannot be null");

        _array = array;
        _currentIndex = 0;
    }

    // Metoda vrátí hodnotu, na kterou iterátor nyní ukazuje
    public T CurrentItem()
    {
        if (IsDone()) // ošetříme, zda není průchod hotový
            throw new IteratorOutOfBoundException();
        else
            return _array[_currentIndex];
    }

    // Metoda resetuje iterátor do původního stavu -
    // přesuneme se na první prvek průchodu
    public void First()
    {
        _currentIndex = 0;
    }

    // Pomocí této metody zjišťujeme, zda není průchod ukončen
    public bool IsDone()
    {
        return _currentIndex >= _array.Length;
    }

    // Metoda posouvá iterátor o jeden prvek dopředu
    public void Next()
    {
        ++_currentIndex;
    }
}

// Konkrétní implementace iterátoru, jež pole prochází pozpátku
// Myšlenka je stejná jako u implementace výše, pouze začínáme od konce
// a při posouvání na další prvek se indexem posouváme blíže k začátku
class ReverseArrayIterator<T> : IIterator<T>
{
    private T[] _array;
    private int _currentIndex;

    public ReverseArrayIterator(T[] array)
    {
        if (array == null)
            throw new ArgumentException("Array cannot be null");

        _array = array;
        _currentIndex = _array.Length - 1;
    }

    public T CurrentItem()
    {
        if (IsDone())
            throw new IteratorOutOfBoundException();
        else
            return _array[_currentIndex];
    }

    public void First()
    {
        _currentIndex = _array.Length - 1;
    }

    public bool IsDone()
    {
        return _currentIndex < 0;
    }

    public void Next()
    {
        --_currentIndex;
    }
}

Konkrétní implementace iterátoru si v konstruktoru nechá předat pole, se kterým bude pracovat a nastaví proměnnou _currentIndex na výchozí hodnotu (0 v klasickém iterátoru, array.Length – 1 v reverzním iterátoru). Metoda First() pouze vrátí index na původní hodnotu. Metoda IsDone() zkontroluje, zda nejsme indexem mimo pole. Poslední metoda Next() posune index v požadovaném směru.

Pro implementaci zde používáme generiky. Pokud s touto technologií nejste obeznámeni, představte si, že místo všech výskytů T se v kódu nachází například int.

Nyní již máme hotovo a zbývá pouze ukázat, jak by se dané iterátory dali použít. Stačí jen vytvořit pole libovolného datového typu, k němu instancovat iterátory a například vypsat hodnoty do konzole.

// Metoda vezme instanci iterátoru a postará se o výpis všech prvků
// Zároveň se zde ukazuje klasické využití iterátorových funkcí v cyklu for
// pro průchod všemi prvky
static void printAllUsingIterator(IIterator<int> iterator)
{
    for (iterator.First(); !iterator.IsDone(); iterator.Next())
        Console.Write(iterator.CurrentItem());
    Console.WriteLine();
}

static void Main(string[] args)
{
    // Prvky, které budeme pomocí iterátorů procházet
    int[] intArr = { 1, 2, 3, 4, 5 };

    // Nejprve použijeme první iterátor a vypíšeme prvky
    Console.WriteLine("Using normal iterator: ");
    IIterator<int> iterator = new ArrayIterator<int>(intArr);
    printAllUsingIterator(iterator);

    // Nyní použijeme reverse iterátor a znovu vypíšeme prvky
    // Můžeme si všimnout, že pro výpis všech prvků používáme stejnou funkci
    // (printAllUsingIterator) a přitom jsou výpisy různé, protože se o logiku 
    // průchodu starají iterátory
    Console.WriteLine("Using reverse iterator: ");
    iterator = new ReverseArrayIterator<int>(intArr);
    printAllUsingIterator(iterator);

    Console.Read();
}

Iterátory najdou své uplatnění při „procházení“ všemožných struktur, a přitom zachovají stejné rozhraní, ať už je vnitřní reprezentace jakákoliv. Jejich použití i implementace není nikterak složitá a vřele doporučuji si je vyzkoušet.