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

2020-07-07

反覆器已經被實踐在許多當代的語言之中,作為集合類型的低階類別所使用。以往並沒有特別感受到 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