迭代器模式
因為這一章涉及到兩個模式,內容有點多,還有一個組合模式留到下一篇寫吧。
有許多種方法可以把對象堆起來成為一個集合(collection)。你可以把它們放進數組、堆棧、列表或者是散列表(Hashtable)中,這是你的自由。每一種都有它自己的優點和適合的使用時機,但總有一個時候,你的客戶想要遍歷這些對象,而當他這么做時,你打算讓客戶看到你的實現嗎?我們當然希望最好不要!這太不專業了。本章的迭代器模式將能讓客戶遍歷你的對象而又無法窺視你存儲對象的方式。
先來看看迭代器模式的定義:
- 提供一種方法順序訪問一個聚合對象中的各個元素,而又不暴露其內部的表示。
題例:有兩家餐廳,披薩店和煎餅店,它們合并了,雖然可以在一個地方同時想用煎餅屋的早餐和餐廳的午餐,但是煎餅屋的菜單用用的ArrayList記錄菜單的,而餐廳用的是數組,而兩家餐館都不愿意修改自己的實現。畢竟有很多代碼依賴它們。
幸好兩家都統一實現了MenuItem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | //菜單項,保存了菜單信息 public class MenuItem { ???? private String name; ???? private String description; ???? private boolean vegetarian; ???? private double price; ? ???? public MenuItem(String name, String description,? boolean vegetarian,? double price) { ???????? super (); ???????? this .name = name; ???????? this .description = description; ???????? this .vegetarian = vegetarian; ???????? this .price = price; ???? } ? ???? public String getName() { ???????? return name; ???? } ? ???? public void setName(String name) { ???????? this .name = name; ???? } ? ???? public String getDescription() { ???????? return description; ???? } ? ???? public void setDescription(String description) { ???????? this .description = description; ???? } ? ???? public boolean isVegetarian() { ???????? return vegetarian; ???? } ? ???? public void setVegetarian( boolean vegetarian) { ???????? this .vegetarian = vegetarian; ???? } ? ???? public double getPrice() { ???????? return price; ???? } ? ???? public void setPrice( double price) { ???????? this .price = price; ???? } } |
再來看看兩家店各自的菜單實現:
煎餅店:用ArrayList
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 煎餅餐店對象,用ArrayList保存了菜單。 public class PancakeHouseMenu { ???? ArrayList<MenuItem> menuItems; ? ???? public PancakeHouseMenu() { ???????? menuItems =? new ArrayList<MenuItem>(); ???????? addItem( "煎餅1號" ,? "牛肉煎餅" ,? false ,? 2.99 ); ???????? addItem( "煎餅2號" ,? "素食煎餅" ,? true ,? 1.49 ); ???? } ? ???? public void addItem(String name, String description,? boolean vegetarian,? double price) { ???????? MenuItem menu =? new MenuItem(name, description, vegetarian, price); ???????? menuItems.add(menu); ???? } ? ???? public ArrayList<MenuItem> getMenuItems() { ???????? return menuItems; ???? } } |
披薩店:用數組
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 披薩餐廳對象,用數組保存了菜單信息。 public class PizzaHouseMenu { ???? static final int MAX_ITEMS =? 2 ; ???? int numberOfItems =? 0 ; ???? MenuItem[] menuItems; ? ???? public PizzaHouseMenu() { ???????? menuItems =? new MenuItem[MAX_ITEMS]; ???? } ? ???? public void addItem(String name, String description,? boolean vegetarian,? double price) { ???????? MenuItem menu =? new MenuItem(name, description, vegetarian, price); ???????? if (numberOfItems >= MAX_ITEMS) ???????????? System.out.println( "對不起,菜單數量已滿" ); ???????? else ???????????? menuItems[numberOfItems++] = menu; ???? } ? ???? public MenuItem[] getMenuItems() { ???????? return menuItems; ???? } } |
有兩種不同的菜單表現方式,這會帶來什么問題?
假設你是一個女招待下面是你做的事,你會怎么辦?
- printMenu(); 打印出菜單上的每一項
- printBreakfastMenu(); 只打印早餐
- printLunchMenu(); 只打印午餐
- printVegetarianMenu(); 打印所有的素食菜單
- isItemVegetarian(name); 查詢指定的菜品是否是素食
指定項的名稱,如果該項是素食的話,返回true,否則返回false
打印沒分菜單上的所有項,必須調用PancakeHouseMenu和PizzaHouseMenu的getMenuItenm()方法,來取得它們各自的菜單項,兩者返回類型是不一樣的。
1 2 3 4 5 | PancakeHouseMenu pancakeHouseMenu= new PancakeHouseMenu(); ArrayList breakfastItems=pancakeHouseMenu.getMenuItems(); ? PizzaHouseMenu pizzaHouseMenu= new PizzaHouseMenu(); MenuIten[] linchItenms=pizzaHouseMenu.getMenuItens(); |
打印菜單需要的數組和集合,用循環將數據一一列出來
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class Client { ???? public static void main(String[] args) { ???????? // 先獲得煎餅餐廳的菜單集合 ???????? PancakeHouseMenu pancakeHouseMenu =? new PancakeHouseMenu(); ???????? ArrayList<MenuItem> menusOfPancake = pancakeHouseMenu.getMenuItems(); ? ???????? // 在獲得披薩餐廳的菜單數組 ???????? PizzaHouseMenu pizzaHouseMenu =? new PizzaHouseMenu(); ???????? MenuItem[] menusOfPizza = pizzaHouseMenu.getMenuItems(); ? ???????? //我們用循環將數據一一列出來 ???????? for ( int i =? 0 ; i < menusOfPancake.size(); i++) { ???????????? MenuItem menu = menusOfPancake.get(i); ???????????? System.out.print(menu.getName() +? ",價格:" ); ???????????? System.out.print(menu.getPrice() +? "," ); ???????????? System.out.print(menu.getDescription() +? "\n" ); ???????? } ???????? System.out.println(); ???????? for ( int i =? 0 ; i < menusOfPizza.length; i++) { ???????????? MenuItem menu = menusOfPizza[i]; ???????????? System.out.print(menu.getName() +? ",價格:" ); ???????????? System.out.print(menu.getPrice() +? "," ); ???????????? System.out.print(menu.getDescription() +? "\n" ); ???????? } ???? } } |
我們總是需要處理這兩個菜單的遍歷,如果還有第三家餐廳以不同的方式實現菜單集合,我們就需要有第三個循環。
可以封裝遍歷嗎?
? 可以封裝變化的部分。很明顯,這里發生的變化是:由不同的集合類型所造成的遍歷。但是,這能夠被封裝嗎?讓我們來看看這個想法……
-
要便利煎餅餐廳,我們需要使用ArrayList的size()和get()方法;
-
要便利披薩餐廳,我們需要使用數組的length字段和在中括號中輸入索引;
-
現在我們創建一個對象,將它稱為迭代器(Iterator),利用它來封裝“遍歷集合內的每個對象的過程”;
想要在餐廳菜單中加入一個迭代器,我們需要先定義迭代器接口,然后為披薩餐廳創建一個迭代器類:
1 2 3 4 | public interface Iterator { ???? boolean hasNext(); ???? Object next(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | public class PizzaIterator? implements Iterator { ? ???? MenuItem[] items; ???? int position =? 0 ; ? ???? public PizzaIterator(MenuItem[] items) { ???????? this .items = items; ???? } ? ???? // 判斷數組下一個索引是否還有元素 ???? public boolean hasNext() { ???????? if (position >= items.length || items[position] ==? null ) ???????????? return false ; ???????? else return true ; ???? } ? ???? // 獲得當前索引位置的元素 ???? public Object next() { ???????? MenuItem item = items[position++]; ???????? return item; ???? } } ? public class PancakeIterator? implements Iterator { ? ???? ArrayList<MenuItem> items; ???? int position =? 0 ; ? ???? public PancakeIterator(ArrayList<MenuItem> items) { ???????? this .items = items; ???? } ? ???? // 判斷數組下一個索引是否還有元素 ???? public boolean hasNext() { ???????? if (position >= items.size() || items.get(position) ==? null ) ???????????? return false ; ???????? else return true ; ???? } ? ???? // 獲得當前索引位置的元素 ???? public Object next() { ???????? MenuItem item = items.get(position); ???????? return item; ???? } } |
創建好迭代器后,改寫披薩餐廳的代碼,創建一個PizzaMenuIterator,并返回給客戶:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public class PizzaHouseMenu { ???? static final int MAX_ITEMS =? 2 ; ???? int numberOfItems =? 0 ; ???? MenuItem[] menuItems; ? ???? public PizzaHouseMenu() { ???????? menuItems =? new MenuItem[MAX_ITEMS]; ???????? addItem( "披薩1號" ,? "素食披薩" ,? true ,? 4.99 ); ???????? addItem( "披薩2號" ,? "海鮮蛤蜊披薩" ,? true ,? 5.99 ); ???? } ? ???? public void addItem(String name, String description,? boolean vegetarian,? double price) { ???????? MenuItem menu =? new MenuItem(name, description, vegetarian, price); ???????? if (numberOfItems >= MAX_ITEMS) ???????????? System.out.println( "對不起,菜單數量已滿" ); ???????? else ???????????? menuItems[numberOfItems++] = menu; ???? } ? ???? public Iterator createIterator() { ???????? return new PizzaIterator(menuItems); ???? } } ? public class PancakeHouseMenu { ???? ArrayList<MenuItem> menuItems; ? ???? public PancakeHouseMenu() { ???????? menuItems =? new ArrayList<MenuItem>(); ???????? addItem( "煎餅1號" ,? "牛肉煎餅" ,? false ,? 2.99 ); ???????? addItem( "煎餅2號" ,? "素食煎餅" ,? true ,? 1.49 ); ???? } ? ???? public void addItem(String name, String description,? boolean vegetarian,? double price) { ???????? MenuItem menu =? new MenuItem(name, description, vegetarian, price); ???????? menuItems.add(menu); ???? } ? ???? public Iterator createIterator() { ???????? return new PancakeIterator(menuItems); ???? } } |
我們不再需要getMenuItems()方法,而是用createIterator()方法代替,用來從菜單項數組創建一個迭代器,并把他返回給客戶,返回迭代器接口。客戶不需要知道餐廳菜單使如何實現維護的,也不需要知道迭代器是如何實現的。客戶只需直接使用這個迭代器遍歷菜單即可。下面修改一下客戶類的調用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class Waitress { ???? PancakeHouseMenu pancake; ???? PizzaHouseMenu pizza; ? ???? public Waitress(PancakeHouseMenu pancake, PizzaHouseMenu pizza) { ???????? this .pancake = pancake; ???????? this .pizza = pizza; ???? } ? ???? public void printMenu() { ???????? Iterator pizzaIterator = pizza.createIterator(); ???????? printMenu(pizzaIterator); ? ???????? Iterator pancakeIterator = pancake.createIterator(); ???????? printMenu(pancakeIterator); ???? } ? ???? private void printMenu(Iterator iterator) { ???????? while (iterator.hasNext()) { ???????????? MenuItem menu = (MenuItem)iterator.next(); ???????????? System.out.print(menu.getName() +? ",價格:" ); ???????????? System.out.print(menu.getPrice() +? "," ); ???????????? System.out.print(menu.getDescription() +? "\n" ); ???????? } ???? } } |
1 2 3 4 5 6 7 8 9 | public class Client { ???? public static void main(String[] args) { ???????? PancakeHouseMenu pancake =? new PancakeHouseMenu(); ???????? PizzaHouseMenu pizza =? new PizzaHouseMenu(); ? ???????? Waitress waitress =? new Waitress(pancake, pizza); ???????? waitress.printMenu(); ???? } } |
輸出結果:

到目前為止,我們將客戶調用與餐廳的菜單數據接口解耦了,客戶調用再也不用為每一個不同數據結構的菜單編寫一套遍歷的代碼了。
到目前為止,我們做了些什么?

我們現在使用一個共同的迭代器接口(Iteraotr)實現了兩個具體類(PizzaIterator和PancakeIterator)。這兩個具體類都實現了各自的hasNext()方法和next()方法。
? 然后再PancakeHouseMenu和PizzaHouseMenu兩個類中,創建一個createIterator()方法返回各自的迭代器,在Waitress類中,使用這兩個餐廳對象返回的迭代器打印菜單。這時Waitress類和Client類再也不需要關心存放菜單的數據結構,之關心能從迭代器中獲得菜單就好。
? 迭代器模式給你提供了一種方法,可以順序訪問一個聚集對象的元素,而又不用知道內部是如何表示的。你已經在前面的兩個菜單實現中看到了這一點。在設計中使用迭代器的影響是明顯的:如果你有一個統一的方法訪問聚合中的每一個對象,你就可以編寫多態的代碼和這些聚合搭配,使用如同前面的printMenu()方法一樣,只要有了迭代器這個方法根本不用管菜單究竟是由數組還是集合或者其他的數據結構來保存的。
? 另外一個對你的設計造成重要影響的,是迭代器模式把在元素之間游走的責任交給迭代器,而不是聚合對象。這不僅讓聚合的接口和實現變得更簡潔,也可以讓聚合更專注它所應該專注的事情上面,而不必去理會遍歷的事情。
? 讓我們檢查類圖,將來龍去脈拼湊出來……

先看看Aggregate接口,有一個共同的接口提供所有的聚合使用,這對客戶代碼是很方便的,將客戶代碼從集合對象的實現解耦。
? 接下來看看ConcreteAggregate類,這個具體聚合持有一個對象的集合,并實現一個方法,利用此方法返回集合的迭代器。每一個具體聚合都要負責實例化一個具體的迭代器,次迭代器能夠便利對象集合。
? 接下來是Iterator接口,這是所有迭代器都必須實現的接口,它包含一些方法,利用這些方法可以在集合元素之間游走。你可以自己設計或者使用java.util.Iterator接口。
? 最后是具體的迭代器,負責遍歷集合。
單一責任
? 如果我們允許我們的聚合實現他們內部的集合,以及相關的操作和遍歷的方法,又會如何?我們已經知道這回增加聚合中的方法個數,但又怎么樣呢?為什么這么做不好?
? 想知道為什么,首選需要認清楚,當我們允許一個類不但要完成自己的事情,還同時要負擔更多的責任時,我們就給這個類兩個變化的原因。如果這個集合變化的話,這個類也必須要改變,如果我們遍歷的方式改變的話,這個類也必須跟著改變。所以,引出了設計原則的中心:
單一責任:一個類只有一個引起變化的原因。
類的每個責任都有改變的潛在區域。超過一個責任,意味著超過一個改變區域。這個原則告訴我們,盡量讓每一個類保持單一責任。
? 內聚(cohesion)這個術語你應該聽過,它用來度量一個類或者模塊緊密地達到單一目的或責任。
? 當一個模塊或一個類被設計成只支持一組相關的功能時,我們說它具有高內聚;反之,當被設計成支持一組不相關的功能時,我們說它具有低內聚。
? 內聚是一個比單一職責更普遍的概念,但兩者其實關系是很密切的。遵守這個原則的類更容易有很高的凝聚力,而且比背負許多職責的低內聚類更容易維護。
? 以上就是迭代器模式的一些內容。