重溫《深入淺出設計模式》反覆器模式 (Book Review of Head First Design Pattern, Iterator Pattern)


  1. 反覆器模式
    1. After Pattern
  2. .NET IEnumerable, IEnumerator Interface
    1. MSDN 上實作 IEnumerable 的範例
  3. 小結
  4. 參考資料

反覆器已經被實踐在許多當代的語言之中,作為集合類型的低階類別所使用。以往並沒有特別感受到 Iterator 的美好,只覺得高階的語法 for ... in ... (python) 方便好用,而有時候得到物件回傳 Iterator 反而不知所措 (例如 Beautiful Soup)。但重溫反覆器模式後,才曉得作為底層的反覆器有多重要,同時理解它也有助於自行實踐整合不同型別的集合類別進行迭代。

logo

反覆器模式

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

反覆器模式的 UML

After Pattern

用來表示菜單項目的類別。

MenuItem.cs

public class MenuItem
{
    private string _name;
    private string _description;
    private bool _vegetarian;
    private double _price;

    public MenuItem(string name,
                    string description,
                    bool vegetarian,
                    double price)
    {
        _name = name;
        _description = description;
        _vegetarian = vegetarian;
        _price = price;
    }

    public string GetName()
    {
        return _name;
    }

    public string GetDescription()
    {
        return _description;
    }

    public double GetPrice()
    {
        return _price;
    }

    public bool IsVegetarian()
    {
        return _vegetarian;
    }
}

IIterator.cs
因為在 .NET 中沒有 Iteraotr Interface 可以被實踐,取而代之的是 IEnumerable,但兩者需要被實踐的方式不同,為貼近書中的範例,於是自己增加一個 IIterator 類別。

public interface IIterator<T>
{
    bool HasNext();
    T Next();
    void Remove();
}

Menu.cs
<- DinerMenu.cs
<- CafeMenu.cs (省略)
<- PancakeHouseMenu.cs (省略)

public interface IMenu
{
    IIterator<MenuItem> CreateIterator();
}

public class DinerMenu : IMenu
{
    private static readonly int MAX_ITEMS = 6;
    private int numberOfItems = 0;
    private MenuItem[] menuItems;

    public DinerMenu()
    {
        menuItems = new MenuItem[MAX_ITEMS];

        AddItem("Vegetarian BLT",
            "(Fakin') Bacon with lettuce & tomato on whole wheat", true, 2.99);
        AddItem("BLT",
            "Bacon with lettuce & tomato on whole wheat", false, 2.99);
        AddItem("Soup of the day",
            "Soup of the day, with a side of potato salad", false, 3.29);
        AddItem("Hotdog",
            "A hot dog, with saurkraut, relish, onions, topped with cheese",
            false, 3.05);
        AddItem("Steamed Veggies and Brown Rice",
            "A medly of steamed vegetables over brown rice", true, 3.99);
        AddItem("Pasta",
            "Spaghetti with Marinara Sauce, and a slice of sourdough bread",
            true, 3.89);
    }

    public void AddItem(string name, string description, bool vegetarian, double price)
    {
        MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
        if (numberOfItems >= MAX_ITEMS)
        {
            Console.WriteLine("Sorry, menu is full!  Can't add item to menu");
        }
        else
        {
            menuItems[numberOfItems] = menuItem;
            numberOfItems = numberOfItems + 1;
        }
    }

    public MenuItem[] GetMenuItems()
    {
        return menuItems;
    }

    public IIterator<MenuItem> CreateIterator()
    {
        // 因為 Array 無法提供 Iterator,因此必須自行實作。
        return new DinerMenuIterator(menuItems);
    }
}

DinerMenuIterator.cs
封裝 DinerMenu 的 CreateIterator 方法演算法,也是對於 IIterator 的實作。

public class DinerMenuIterator : IIterator<MenuItem>
{
    private readonly MenuItem[] _list;
    private int position = 0;

    public DinerMenuIterator(MenuItem[] list)
    {
        _list = list;
    }

    public bool HasNext()
    {
        if (position >= _list.Length || _list[position] == null)
        {
            return false;
        }
        else
        {
            return true;
        }
    }

    public MenuItem Next()
    {
        MenuItem menuItem = _list[position];
        position += 1;
        return menuItem;
    }

    public void Remove()
    {
        if (position <= 0)
        {
            throw new Exception("You can't remove an item until you've done at least one next()");
        }
        if (_list[position - 1] != null)
        {
            for (int i = position - 1; i < (_list.Length - 1); i++)
            {
                _list[i] = _list[i + 1];
            }
            _list[_list.Length - 1] = null;
        }
    }
}

Waitress.cs
用以整合不同的 Collections (Aggregations) 類別,透過 IIterator 使用多型的方式呈現不同 Collections 的 MenuItem。

public class Waitress
{
  IMenu _pancakeHouseMenu;
  IMenu _dinerMenu;
  IMenu _cafeMenu;

  public Waitress(IMenu pancakeHouseMenu, IMenu dinerMenu, IMenu cafeMenu)
  {
    _pancakeHouseMenu = pancakeHouseMenu;
    _dinerMenu = dinerMenu;
    _cafeMenu = cafeMenu;
  }

  public void PrintMenu()
  {
    IIterator<MenuItem> pancakeIterator = _pancakeHouseMenu.CreateIterator();
    IIterator<MenuItem> dinerIterator = _dinerMenu.CreateIterator();
    IIterator<MenuItem> cafeIterator = _cafeMenu.CreateIterator();
    Console.WriteLine();
    Console.WriteLine("MENU\n----\nBREAKFAST");
    PrintMenu(pancakeIterator);
    Console.WriteLine("\nLUNCH");
    PrintMenu(dinerIterator);
    Console.WriteLine("\nDINNER");
    PrintMenu(cafeIterator);
  }

  private void PrintMenu(IIterator<MenuItem> iterator)
  {
    while (iterator.HasNext())
    {
      MenuItem menuItem = iterator.Next();
      Console.WriteLine(menuItem.GetName() + ", ");
      Console.WriteLine(menuItem.GetPrice() + " -- ");
      Console.WriteLine(menuItem.GetDescription());
    }
  }
}

.NET IEnumerable, IEnumerator Interface

IEnumerable 僅有一個方法需要實作 GetEnumerator,此方法會回傳 IEnumerator ;而 IEnumerator 則有 Current Properties 以及 MoveNextReset 方法。

MSDN 上實作 IEnumerable 的範例

namespace System.Collections.Generic
{
    public interface IEnumerable<out T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator();
    }
}

public class People : IEnumerable
{
    private Person[] _people;
    public People(Person[] pArray)
    {
        _people = new Person[pArray.Length];

        for (int i = 0; i < pArray.Length; i++)
        {
            _people[i] = pArray[i];
        }
    }

    // Implementation for the GetEnumerator method.
    IEnumerator IEnumerable.GetEnumerator()
    {
       return (IEnumerator) GetEnumerator();
    }

    public PeopleEnum GetEnumerator()
    {
        return new PeopleEnum(_people);
    }
}

public class PeopleEnum : IEnumerator
{
    public Person[] _people;

    // Enumerators are positioned before the first element
    // until the first MoveNext() call.
    int position = -1;

    public PeopleEnum(Person[] list)
    {
        _people = list;
    }

    public bool MoveNext()
    {
        position++;
        return (position < _people.Length);
    }

    public void Reset()
    {
        position = -1;
    }

    object IEnumerator.Current
    {
        get
        {
            return Current;
        }
    }

    public Person Current
    {
        get
        {
            try
            {
                return _people[position];
            }
            catch (IndexOutOfRangeException)
            {
                throw new InvalidOperationException();
            }
        }
    }
}

IEnumerable Interface

小結

✔️將變動的部分封裝起來
✔️類別應該對需求保持開放;應該對既有的程式碼修改保持關閉
✔️多用合成,少用繼承
✔️讓需要互動的物件之間的關係鬆綁
✔️針對介面撰寫,而非針對實踐方式撰寫

參考資料

GitHub - Head-First-Design-Patterns
sourcemaking