本文的概念來自深入淺出設計模式一書 項目需求 有兩個飯店合併了, 它們各自有自己的菜單. 飯店合併之後要保留這兩份菜單. 這兩個菜單是這樣的: 菜單項MenuItem的代碼是這樣的: 最初我們是這樣設計的, 這是第一份菜單: 這是第2份菜單: 同時有兩個菜單存在的問題 問題就是多個菜單把事情變複雜了 ...
本文的概念來自深入淺出設計模式一書
項目需求
有兩個飯店合併了, 它們各自有自己的菜單. 飯店合併之後要保留這兩份菜單.
這兩個菜單是這樣的:
菜單項MenuItem的代碼是這樣的:
最初我們是這樣設計的, 這是第一份菜單:
這是第2份菜單:
同時有兩個菜單存在的問題
問題就是多個菜單把事情變複雜了. 例如: 如果一個服務員需要使用兩份菜單的話, 那麼她就無法很快的告訴客戶有哪些菜是適合素食主義者的了.
服務員還有可能有這些需求:
列印菜單, 列印早餐菜單, 列印午餐菜單, 列印素食菜單, 判斷某個菜是否是素食的.
首先我們嘗試一下如何實現列印菜單:
1. 調用兩個菜單上面的getMenuItem()方法來獲取各自的菜單項, 由於它們的菜單不同, 所以需要寫兩段代碼:
2. 列印兩個菜單的菜單項, 同樣也是兩套代碼:
3. 如果還有一份菜單, 那麼就需要寫三套代碼....
現在就很麻煩了.
怎麼解決這個問題
如果能找到一種方式讓這兩個菜單同時實現一個介面就好了. 我們已經知道, 要把變化的部分封裝起來.
什麼是變化的部分? 由於不同對象集合引起的遍歷操作.
那我們試試;
1. 想要遍歷早餐項, 我們使用ArrayList的size()和get()方法:
2. 想要遍歷午餐項, 我們需要使用Array的length成員變數以及通過索引訪問數組:
3. 如果我們創建一個對象, 把它叫做迭代器, 讓它來封裝我們遍歷集合的方式怎麼樣?
這裡, 我們需要早餐菜單創建一個迭代器, 如果還有剩餘的菜單項沒有遍歷完, 就獲取下一個菜單項.
4. 讓我們在Array上試試:
初識迭代器模式
首先你需要知道這種模式依賴於一個迭代器介面. 例如這個:
hasNext()方法告訴我們集合中是否還有剩餘的條目沒有遍歷到.
next()方法返回下一個條目.
有了這個介面, 我們可以在任何一種集合上實現該介面.:
修改代碼
定義迭代器介面:
然後再DinerMenu上實現迭代器介面:
然後使用迭代器來修改DinerMenu菜單:
註意: 不要直接返回集合, 因為這樣會暴露內部實現.
createIterator()方法返回的是迭代器的介面, 客戶並不需要知道DinerMenu是如何維護菜單項的, 也不需要DinerMenu的迭代器是如何實現的. 它只是用迭代器來遍歷菜單裡面的條目.
最後服務員的代碼如下:
測試代碼:
我們做了哪些修改?
我們只是為菜單添加了createIterator()方法.
而現在, 菜單的實現被封裝了, 服務員不知道菜單是如何保存菜單項的.
我們所需要的只是一個迴圈, 它可以多態的處理實現了迭代器介面的集合.
而服務員使用的是迭代器介面.
現在呢, 菜單還沒有共同的介面, 這意味著服務員仍然被綁定在兩個具體的菜單類上, 一會我們再說這個.
當前的設計圖
目前就是兩個菜單實現了同一套方法, 但是還沒有實現同一個介面.
使用C#, .NET Core控制台項目進行實現
菜單項 MenuItem:
namespace IteratorPattern.Menus { public class MenuItem { public string Name { get; } public string Description { get; } public bool Vegetarian { get; } public double Price { get; } public MenuItem(string name, string description, bool vegetarian, double price) { Name = name; Description = description; Vegetarian = vegetarian; Price = price; } } }
迭代器介面 IMyIterator:
namespace IteratorPattern.Abstractions { public interface IMyIterator { bool HasNext(); object Next(); } }
兩個菜單迭代器:
using IteratorPattern.Abstractions; using IteratorPattern.Menus; namespace IteratorPattern.MenuIterators { public class MyDinerMenuIterator: IMyIterator { private readonly MenuItem[] _menuItems; private int _position; public MyDinerMenuIterator(MenuItem[] menuItems) { _menuItems = menuItems; } public bool HasNext() { if (_position >= _menuItems.Length || _menuItems[_position] == null) { return false; } return true; } public object Next() { var menuItem = _menuItems[_position]; _position++; return menuItem; } } } using System.Collections; using IteratorPattern.Abstractions; namespace IteratorPattern.MenuIterators { public class MyPancakeHouseMenuIterator:IMyIterator { private readonly ArrayList _menuItems; private int _position; public MyPancakeHouseMenuIterator(ArrayList menuItems) { _menuItems = menuItems; } public bool HasNext() { if (_position >= _menuItems.Count || _menuItems[_position] == null) { return false; } _position++; return true; } public object Next() { var menuItem = _menuItems[_position]; _position++; return menuItem; } } }
兩個菜單:
using System; using System.Collections.Generic; using System.Text; using IteratorPattern.Abstractions; using IteratorPattern.MenuIterators; namespace IteratorPattern.Menus { public class MyDinerMenu { private const int MaxItems = 6; private int _numberOfItems = 0; private MenuItem[] MenuItems { get; } public MyDinerMenu() { MenuItems = new MenuItem[MaxItems]; 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); } public void AddItem(string name, string description, bool vegetarian, double price) { var menuItem = new MenuItem(name, description, vegetarian, price); if (_numberOfItems >= MaxItems) { Console.WriteLine("Sorry, menu is full! Can't add item to menu"); } else { MenuItems[_numberOfItems] = menuItem; _numberOfItems++; } } public IMyIterator CreateIterator() { return new MyDinerMenuIterator(MenuItems); } } } using System.Collections; using IteratorPattern.Abstractions; using IteratorPattern.MenuIterators; namespace IteratorPattern.Menus { public class MyPancakeHouseMenu { public ArrayList MenuItems { get; } public MyPancakeHouseMenu() { MenuItems = new ArrayList(); AddItem("K&B’s Pancake Breakfast", "Pancakes with scrambled eggs, and toast", true, 2.99); AddItem("Regular Pancake Breakfast", "Pancakes with fried eggs, sausage", false, 2.99); AddItem("Blueberry Pancakes", "Pancakes made with fresh blueberries", true, 3.49); AddItem("Waffles", "Waffles, with your choice of blueberries or strawberries", true, 3.59); } public void AddItem(string name, string description, bool vegetarian, double price) { var menuItem = new MenuItem(name, description, vegetarian, price); MenuItems.Add(menuItem); } public IMyIterator CreateIterator() { return new MyPancakeHouseMenuIterator(MenuItems); } } }
服務員 Waitress:
using System; using IteratorPattern.Abstractions; using IteratorPattern.Menus; namespace IteratorPattern.Waitresses { public class MyWaitress { private readonly MyPancakeHouseMenu _pancakeHouseMenu; private readonly MyDinerMenu _dinerMenu; public MyWaitress(MyPancakeHouseMenu pancakeHouseMenu, MyDinerMenu dinerMenu) { _pancakeHouseMenu = pancakeHouseMenu; _dinerMenu = dinerMenu; } public void PrintMenu() { var pancakeIterator = _pancakeHouseMenu.CreateIterator(); var dinerIterator = _dinerMenu.CreateIterator(); Console.WriteLine("MENU\n--------------\nBREAKFIRST"); PrintMenu(pancakeIterator); Console.WriteLine("\nLUNCH"); PrintMenu(dinerIterator); } private void PrintMenu(IMyIterator iterator) { while (iterator.HasNext()) { var menuItem = iterator.Next() as MenuItem; Console.Write($"{menuItem?.Name}, "); Console.Write($"{menuItem?.Price} -- "); Console.WriteLine($"{menuItem?.Description}"); } } } }
測試:
static void MenuTestDriveUsingMyIterator() { var pancakeHouseMenu = new MyPancakeHouseMenu(); var dinerMenu = new MyDinerMenu(); var waitress = new MyWaitress(pancakeHouseMenu, dinerMenu); waitress.PrintMenu(); }
做一些改進
Java裡面內置了Iterator介面, 我們剛纔是手寫了一個Iterator迭代器介面. Java內置的定義如下:
註意裡面這個remove()方法, 我們可能不需要它.
remove()方法是可選實現的, 如果你不想讓集合有此功能的話, 就應該拋出NotSupportedException(C#的).
使用java內置的Iterator來實現
由於PancakeHouseMenu使用的是ArrayList, 而ArrayList已經實現了該介面, 那麼:這樣簡單改一下就可以:
針對DinerMe菜單, 還是需要手動實現的:
最後別忘了給菜單規定一個統一的介面:
服務員Waitress類裡面也使用Menu來代替具體的菜單, 這樣也減少了服務員對具體類的依賴(針對介面編程, 而不是具體的實現):
最後看下改進後的設計類圖:
迭代器模式定義
迭代器模式提供了一種訪問聚合對象(例如集合)元素的方式, 而且又不暴露該對象的內部表示.
迭代器模式負責遍歷該對象的元素, 該項工作由迭代器負責而不是由聚合對象(集合)負責.
類圖:
其它問題
- 迭代器分內部迭代器和外部迭代器, 我們上面實現的是外部迭代器. 也就是說客戶控制著迭代, 它通過調用next()方法來獲取下個元素. 而內部迭代器由迭代器本身自己控制迭代, 這種情況下, 你需要告訴迭代器遍歷的時候需要做哪些動作, 所以你得找到一種方式把操作傳遞進去. 內部迭代器還是不如外部的靈活, 但是也許使用起來會簡單一些?
- 迭代器意味著無序. 它所遍歷的集合的順序是根據集合來定的, 也有可能會遍歷出來的元素值會重覆.
單一職責設計原則
一個類應該只有一個變化發生的原因.
寫代碼的時候這個原則很容易被忽略掉, 只能通過多檢查設計來避免違反原則.
所謂的高內聚, 就是只這個類是圍繞一套關連的函數而設計的.
而低內聚就是只這個類是圍繞一些不相關的函數而設計的.
遵循該原則的類通常是高內聚的, 並且可維護性要比那些多重職責或低內聚的類好.
需求變更
還需要添加另一份菜單:
這個菜單使用的是HashTable.
首先修改該菜單, 讓它實現Menu介面:
註意看HashTable的不同之處:
首先通過values()方法獲取HashTable的集合對象, 這個對象正好實現了Iterator介面, 直接調用iterator()方法即可.
最後修改服務員類:
測試:
到目前我們做了什麼
我們給了服務員一種簡單的方式來遍歷菜單項, 不同的菜單實現了同一個迭代器介面, 服務員不需要知道菜單項的實現方法.
我們把服務員和菜單的實現解耦了
而且使服務員可以擴展:
還有個問題
現在有三個菜單, 每次再添加一個菜單的時候, 你都得相應的添加一套代碼, 這違反了"對修改關閉, 對擴展開放原則".
那我們把這些菜單放到可迭代的集合即可:
C#, .NET Core控制帶項目實現
菜單介面:
using System.Collections; namespace IteratorPattern.Abstractions { public interface IMenu { IEnumerator CreateIEnumerator(); } }
三個菜單:
using System; using System.Collections; using IteratorPattern.Abstractions; using IteratorPattern.MenuIterators; namespace IteratorPattern.Menus { public class DinerMenu: IMenu { private const int MaxItems = 6; private int _numberOfItems = 0; private MenuItem[] MenuItems { get; } public DinerMenu() { MenuItems = new MenuItem[MaxItems]; 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); } public void AddItem(string name, string description, bool vegetarian, double price) { var menuItem = new MenuItem(name, description, vegetarian, price); if (_numberOfItems >= MaxItems) { Console.WriteLine("Sorry, menu is full! Can't add item to menu"); } else { MenuItems[_numberOfItems] = menuItem; _numberOfItems++; } } public IEnumerator CreateIEnumerator() { return new DinerMenuIterator(MenuItems); } } } using System.Collections; using IteratorPattern.Abstractions; using IteratorPattern.MenuIterators; namespace IteratorPattern.Menus { public class PancakeHouseMenu: IMenu { public ArrayList MenuItems { get; } public PancakeHouseMenu() { MenuItems = new ArrayList(); AddItem("K&B’s Pancake Breakfast", "Pancakes with scrambled eggs, and toast", true, 2.99); AddItem("Regular Pancake Breakfast", "Pancakes with fried eggs, sausage", false, 2.99); AddItem("Blueberry Pancakes", "Pancakes made with fresh blueberries", true, 3.49); AddItem("Waffles", "Waffles, with your choice of blueberries or strawberries", true, 3.59); } public void AddItem(string name, string description, bool vegetarian, double price) { var menuItem = new MenuItem(name, description, vegetarian, price); MenuItems.Add(menuItem); } public IEnumerator CreateIEnumerator() { return new PancakeHouseMenuIterator(MenuItems); } } } using System.Collections; using IteratorPattern.Abstractions; namespace IteratorPattern.Menus { public class CafeMenu : IMenu { public Hashtable MenuItems { get; } = new Hashtable(); public CafeMenu() { AddItem("Veggie Burger and Air Fries", "Veggie burger on a whole wheat bun, lettuce, tomato, and fries", true, 3.99); AddItem("Soup of the day", "A cup of the soup of the day, with a side salad", false, 3.69); AddItem("Burrito", "A large burrito, with whole pinto beans, salsa, guacamole", true, 4.29); } public IEnumerator CreateIEnumerator() { return MenuItems.GetEnumerator(); } public void AddItem(string name, string description, bool vegetarian, double price) { var menuItem = new MenuItem(name, description, vegetarian, price); MenuItems.Add(menuItem.Name, menuItem); } } }
菜單的迭代器:
using System; using System.Collections; using IteratorPattern.Menus; namespace IteratorPattern.MenuIterators { public class DinerMenuIterator: IEnumerator { private readonly MenuItem[] _menuItems; private int _position = -1; public DinerMenuIterator(MenuItem[] menuItems) { _menuItems = menuItems; } public bool MoveNext() { _position++; if (_position >= _menuItems.Length || _menuItems[_position] == null) { return false; } return true; } public void Reset() { _position = -1; } public object Current => _menuItems[_position]; } }
using System.Collections; using System.Collections.Generic; namespace IteratorPattern.MenuIterators { public class PancakeHouseMenuIterator : IEnumerator { private readonly ArrayList _menuItems; private int _position = -1; public PancakeHouseMenuIterator(ArrayList menuItems) { _menuItems = menuItems; } public bool MoveNext() { _position++; if (_position >= _menuItems.Count || _menuItems[_position] == null) { return false; } return true; } public void Reset() { _position = -1; } public object Current => _menuItems[_position]; } }
服務員:
using System; using System.Collections; using IteratorPattern.Abstractions; using IteratorPattern.Menus; namespace IteratorPattern.Waitresses { public class Waitress { private readonly ArrayList _menus; public Waitress(ArrayList menus) { _menus = menus; } public void PrintMenu() { var menuIterator = _menus.GetEnumerator(); while (menuIterator.MoveNext()) { var menu = menuIterator.Current as IMenu; PrintMenu(menu?.CreateIEnumerator()); } } private void PrintMenu(IEnumerator iterator) { while (iterator.MoveNext()) { if (iterator.Current != null) { MenuItem menuItem; if (iterator.Current is MenuItem item) { menuItem = item; } else { menuItem = ((DictionaryEntry)iterator.Current).Value as MenuItem; } Console.Write($"{menuItem?.Name}, "); Console.Write($"{menuItem?.Price} -- "); Console.WriteLine($"{menuItem?.Description}"); } } Console.WriteLine(); } } }
測試:
static void MenuTestDriveUsingIEnumerator() { var pancakeHouseMenu = new PancakeHouseMenu(); var dinerMenu = new DinerMenu(); var cafeMenu = new CafeMenu(); var waitress = new Waitress(new ArrayList(3) { pancakeHouseMenu, dinerMenu, cafeMenu }); waitress.PrintMenu(); }
深入淺出設計模式的C#實現的代碼: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp
這篇先到這, 本章涉及到組合模式, 下篇文章再寫.