深入淺出設計模式(Design Pattern)-迭代器模式(10-Iterator-Pattern)

Ben
生活的各種可能性
19 min readMar 1, 2024
photo by 愚木混株 cdd20

不知道你有沒有遇過需要迭代的物件,但迭代的方式不太一樣,這篇要來介紹把迭代這個動作封裝起來的模式,迭代器模式(Iterator Pattern)。

迭代器模式(Iterator Pattern)

定義:提供一種方式讓你依序存取物件集合的元素,而且不會公開他的底層表示法。

我們來看看這次的情境內容:我們有兩家餐廳,他們各自有不同的菜單內容,但是他們使用不同的方式來儲存菜單的項目。

我們的菜單的項目包含一些資訊以及方法:

class MenuItem {
name: string;
description: string;
vegetarian: boolean;
price: number;

constructor(name: string, des: string, vegetarian: boolean, price: number) {
this.name = name;
this.description = des;
this.vegetarian = vegetarian;
this.price = price;
}

public getName(): string {
return this.name;
}

public getDescription(): string {
return this.description;
}

public getPrice(): number {
return this.price;
}

public isVegetarian(): boolean {
return this.vegetarian;
}
}

但每一間餐廳的儲存項目的資料格式不太一樣,例如在這本書當中,分別是以ArrayList和Array兩種不同的格式來儲存資料,在Java中Array是一個必須先宣告長度的陣列,且不能再更改。ArrayList則跟JavaScript裡的陣列概念是差不多的。

而這個模式可以解決我們的問題,它關注在封裝迭代這個行為,應用抽象讓每個實例去實現屬於自己的迭代器。

首先我們會創建一個抽象的介面,讓每個需要被迭代的類別都必須實作它,它分別有hasNext(是否有下一個)以及next(返回下一個Item)兩個方法必須去實作。

interface MenuIterator {
hasNext(): boolean;
next(): MenuItem;
}

接著我們的每個菜單都必須實作自己的MenuIterator,我們以DinnerMenu為例,建構式接受一個T型態(MenuItem)的陣列,並且有menuItem(目前的item)以及position(目前在陣列中位置)。

// Dinner的Iterator,extends MenuItem並且實作介面MenuIterator
class DinnerMenuIterator<T extends MenuItem> implements MenuIterator {
items: T[] = [];
menuItem: T | null = null;
position: number = 0;

constructor(items: T[]) {
this.items = items;
}

public next(): MenuItem {
this.menuItem = this.items[this.position];
this.position += 1;

return this.menuItem;
}

public hasNext(): boolean {
return this.position < this.items.length;
}

public getOrder(): number {
return this.position;
}
}

首先我們的每個菜單應該要有產生自己迭代器的方法,而每個菜單也必須實作這個方法,我們將它宣告成一個介面,讓每個菜單都必須實作它。

這個介面接受泛型T並且抽象的createIterator方法回傳的值也是T類型。

interface Menu<T> {
createIterator(): T;
}

接著我們實作DinnerMenu。在它的建構式中會調用它自己的方法新增菜單的品項,同時也會實作產生自己迭代器的方法。

// 實作Menu這個介面,該介面接受T型態的泛型
class DinnerMenu implements Menu<MenuIterator> {
static MAX_ITEMS: number = 6;
numberOfItems: number = 0;
menuItems: MenuItem[] = [];

constructor() {
this.addItem("Vegetarian BLT", "Bacon with lettuce & tomato on whole wheat", true, 2.99);
this.addItem("BLT", "Bacon with lettuce & tomato on whole wheat", false, 2.99);
this.addItem("Soup of the day", "Soup of the day, with a side of potato salad", false, 3.29);
this.addItem("Hotdog", "A hot dog, with sauerkraut, relish, onions, topped with cheese", false, 3.05);
}

public addItem(name: string, des: string, vegetarian: boolean, price: number): void {
const menuItem: MenuItem = new MenuItem(name, des, vegetarian, price);

if (this.numberOfItems >= DinnerMenu.MAX_ITEMS) {
console.log("Sorry, menu is full! Can't add item to menu");
} else {
this.menuItems[this.numberOfItems] = menuItem;
this.numberOfItems += 1;
}
}

// 實作產生迭代器的方法
public createIterator(): MenuIterator {
return new DinnerMenuIterator(this.menuItems)
}
}

接下來菜單差不多完成了,但是我們必須要有個服務生來幫我們提供菜單給顧客,這個服務生只關注在提供菜單這個動作,它並不在乎菜單的內容是什麼也不在乎菜單怎麼迭代的(與菜單和迭代器都解耦合)。

它的建構式中接受並初始化兩個菜單,並且擁有兩個方法:

printMenu:對外提供了一個printMenu的方法,也就是剛剛提到的服務生的職責,它只會關注在提供菜單內容這個行為,它會調用自身的私有方法printMenuDetail去使用菜單迭代器提供的方法去迭代菜單項目。

printMenuDetail:使用菜單提供的迭代器去迭代物件內容。

// waitress參考Menu<MenuIterator>(指向同一個介面),只需要關注如何提供菜單
class Waitress<T extends Menu<U>, U extends MenuIterator> {
pancakeHouseMenu: T;
dinnerMenu: T;

// 建構式接受兩個菜單
constructor(pancakeHouseMenu: T, dinnerMenu: T) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinnerMenu = dinnerMenu;
}

public printMenu(): void {
const pancakeIterator: U = this.pancakeHouseMenu.createIterator();
const dinnerIterator: U = this.dinnerMenu.createIterator();

console.log("*BREAKFAST MENUS*");
this.printMenuDetail(pancakeIterator);
console.log("-----------------------");

console.log("*DINNER MENUS*");
this.printMenuDetail(dinnerIterator);
console.log("-----------------------");
}

private printMenuDetail(iterator: U): void {
while (iterator.hasNext()) {
const menuItem = iterator.next();
console.log(`${iterator.getOrder()}: ${menuItem.getName()}, ${menuItem.getPrice()} -- ${menuItem.getDescription()}`);
}
}
}

這樣我們就達成了我們需要不同迭代方式去迭代物件的需求了。

我們來看看它的類別圖,Aggregate(Menu)與Iterator(MenuIterator)介面分別用來解開用戶端和迭代器以及迭代器和菜單之間的耦合。我們關注在該類別有沒有實作抽象,避免實體之間的互相依賴,它們分別都只關注在自己的任務上。

類別關係示意圖

在這個模式中將迭代這個任務交給了迭代器,而不是集合物件本身,讓集合物件關注在他應該關注的事情上(管理一堆物件),而不是迭代。

今天假如我們在集合內部實作相關的操作以及迭代的方法又會怎麼樣?而為什麼不好?

我們必須了解到當類別不僅要負責自己的工作(管理集合)的同時也要承擔其他責任(迭代)時,那個類別就會有兩個改變的理由。它可能在集合改變時改變,也可能在迭代的方法改變時改變。

設計原則:類別只應該有一個改變的理由。

單一責任原則

我們想要避免類別裡有任何改變,因為修改會創造各種機讓問題滲透進來。如果類別有兩個改變的理由,它將來就更有機會改變,而且那個改變會影響設計的兩個層面。

這條原則告訴我們將每一個職責指派給一個類別,而且只能指派給一個類別。聽起來雖然很簡單,但在設計中將職責切開是最困難的事情之一,因為我們的頭腦習慣一次看著一組行為,並且將它們集中在一起。所以我們必須認真的檢查我們的設計,並且在系統成長時,注意類別以超過一種方式改變的訊號。

內聚力

內聚力是衡量一個類別或模組多麽支持單一目的或職責的指標。

例如果一個模組或類別是用一組相關的功能來設計的,它就具有高內聚力,如果它是用一組不相關的功能來設計的,它就是低內聚力。

而內聚力是單一責任原則的廣義概念,但是它們兩者有密切的關係。與具備多個職責且低內聚力的類別相比,遵守這條原則的類別往往具有高內聚力,而且更容易維護。

以下是這次完整的範例:

// Iterator介面:每個菜單的迭代器都必須實作他,只需關注迭代這個動作
interface MenuIterator {
hasNext(): boolean;
next(): MenuItem;
getOrder(): number;
}

// Menu介面:每個菜單也必須實作createIterator,只需關注如何創造Iterator
interface Menu<T> {
createIterator(): T;
}

// 封裝每個menuItem
class MenuItem {
name: string;
description: string;
vegetarian: boolean;
price: number;

constructor(name: string, des: string, vegetarian: boolean, price: number) {
this.name = name;
this.description = des;
this.vegetarian = vegetarian;
this.price = price;
}

public getName(): string {
return this.name;
}

public getDescription(): string {
return this.description;
}

public getPrice(): number {
return this.price;
}

public isVegetarian(): boolean {
return this.vegetarian;
}
}

// 實作DinnerMenu的Iterator
class DinnerMenuIterator<T extends MenuItem> implements MenuIterator {
items: T[] = [];
menuItem: T | null = null;
position: number = 0;

constructor(items: T[]) {
this.items = items;
}

public next(): MenuItem {
this.menuItem = this.items[this.position];
this.position += 1;

return this.menuItem;
}

public hasNext(): boolean {
return this.position < this.items.length;
}

public getOrder(): number {
return this.position;
}
}

// 實作DinnerMenu
class DinnerMenu implements Menu<MenuIterator> {
static MAX_ITEMS: number = 6;
numberOfItems: number = 0;
menuItems: MenuItem[] = [];

constructor() {
this.addItem("Vegetarian BLT", "Bacon with lettuce & tomato on whole wheat", true, 2.99);
this.addItem("BLT", "Bacon with lettuce & tomato on whole wheat", false, 2.99);
this.addItem("Soup of the day", "Soup of the day, with a side of potato salad", false, 3.29);
this.addItem("Hotdog", "A hot dog, with sauerkraut, relish, onions, topped with cheese", false, 3.05);
}

public addItem(name: string, des: string, vegetarian: boolean, price: number): void {
const menuItem: MenuItem = new MenuItem(name, des, vegetarian, price);

if (this.numberOfItems >= DinnerMenu.MAX_ITEMS) {
console.log("Sorry, menu is full! Can't add item to menu");
} else {
this.menuItems[this.numberOfItems] = menuItem;
this.numberOfItems += 1;
}
}

public createIterator(): MenuIterator {
return new DinnerMenuIterator(this.menuItems)
}
}

// 實作PancakeMenu
class PancakeHouseMenu implements Menu<MenuIterator> {
menuItems: MenuItem[] = [];

constructor() {
this.addItem("K&B's Pancake Breakfast", "Pancakes with scrambled eggs and toast", true, 2.99);
this.addItem("Regular Pancake Breakfast", "Pancakes with fried eggs, sausage", false, 2.99);
this.addItem("Blueberry Pancakes", "Pancakes made with fresh blueberries", true, 3.49);
this.addItem("Waffles", "Waffles with your choice of blueberries or strawberries", true, 3.49);
}

public addItem(name: string, des: string, vegetarian: boolean, price: number): void {
const menuItem: MenuItem = new MenuItem(name, des, vegetarian, price);
this.menuItems.push(menuItem);
}

public createIterator(): MenuIterator {
return new PancakeHouseIterator(this.menuItems);
}
}

// 實作PancakeMenu的Iterator
class PancakeHouseIterator<T extends MenuItem> implements MenuIterator {
items: T[] = [];
menuItem: T | null = null;
position: number = 0;

constructor(items: T[]) {
this.items = items;
}

public next(): MenuItem {
this.menuItem = this.items[this.position];
this.position += 1;
return this.menuItem;
}

public hasNext(): boolean {
return this.position < this.items.length;
}

public getOrder(): number {
return this.position;
}
}

// waitress參考Menu<MenuIterator>(指向同一個介面),只需要關注如何提供菜單(與迭代器和菜單都解耦合)
class Waitress<T extends Menu<U>, U extends MenuIterator> {
pancakeHouseMenu: T;
dinnerMenu: T;

// 建構式接受兩個菜單
constructor(pancakeHouseMenu: T, dinnerMenu: T) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinnerMenu = dinnerMenu;
}

public printMenu(): void {
const pancakeIterator: U = this.pancakeHouseMenu.createIterator();
const dinnerIterator: U = this.dinnerMenu.createIterator();

console.log("*BREAKFAST MENUS*");
this.printMenuDetail(pancakeIterator);
console.log("-----------------------");

console.log("*DINNER MENUS*");
this.printMenuDetail(dinnerIterator);
console.log("-----------------------");
}

private printMenuDetail(iterator: U): void {
while (iterator.hasNext()) {
const menuItem = iterator.next();
console.log(`${iterator.getOrder()}: ${menuItem.getName()}, ${menuItem.getPrice()} -- ${menuItem.getDescription()}`);
}
}
}

const pancakeHouseMenu = new PancakeHouseMenu();
const dinnerMenu = new DinnerMenu();
const waitress = new Waitress(pancakeHouseMenu, dinnerMenu);
waitress.printMenu();

重點提示

  • 迭代器可讓你接觸集合的元素,同時又不公開它的內部結構。
  • 迭代器可接受迭代一個集合的工作,並將它封裝在另一個物件裡。
  • 在使用迭代器時,我們讓集合負責提供遍歷其資料的操作。
  • 迭代器提供共同的介面來遍歷集合的項目,可讓你在寫程式時,利用多型來使用集合項目。
  • 我們應該盡力讓每一個類別都只有一個責任。

結語

在迭代器模式中將單一責任原則做了一個很好的詮釋,這個原則可以確保我們在專案不斷擴大時候避免許多不必要的麻煩,在可讀性、擴充性以及我們想要盡量避免的修改行為上都有很大的幫助。當理由越少,改變的機會和可能性就越少。

下一篇要介紹:組合模式(Composite Pattern)

這篇就到這邊瞜,如果有任何問題歡迎留言給我,喜歡的話也別忘了clap起來唷。

--

--