《Head first设计模式》学习笔记 – 迭代器模式
迭代器模式提供一種方法順序訪問一個聚合對象中的各個元素,而又不暴露其內(nèi)部的表示。
爆炸性新聞:對象村餐廳和對象村煎餅屋合并了!
真是個好消息!現(xiàn)在我們可以在同一個地方,享用煎餅屋美味的煎餅早餐,和好吃的餐廳午餐了。但是,好像有一點小麻煩:
新的餐廳想用煎餅屋菜單當作早餐的菜單,使用餐廳的菜單當做午餐的菜單,大家都同意了這樣實現(xiàn)菜單項。但是大家無法同意菜單的實現(xiàn)。煎餅屋使用ArrayList記錄他的菜單項,而餐廳使用的是數(shù)組。他們兩個都不愿意改變他們的實現(xiàn),畢竟有太多代碼依賴于它們了。
檢查菜單項
讓我們先檢查每份菜單上的項目和實現(xiàn)。
| 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 | public class MenuItem { ????// 名稱 ????String name; ????// 描述 ????String description; ????// 是否為素食 ????boolean vegetarian; ????// 價格 ????double price; ????public MenuItem(String name, ????????????????????String description, ????????????????????boolean vegetarian, ????????????????????double price) { ????????this.name = name; ????????this.description = description; ????????this.vegetarian = vegetarian; ????????this.price = price; ????} ????public String getName() { ????????return name; ????} ????public String getDescription() { ????????return description; ????} ????public double getPrice() { ????????return price; ????} ????public boolean isVegetarian() { ????????return vegetarian; ????} } |
兩個餐廳的菜單實現(xiàn)
我們先來看看兩個餐廳的菜單實現(xiàn)
| 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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | // 這是煎餅屋的菜單實現(xiàn) public class PancakeHouseMenu { ????// 煎餅屋使用一個ArrayList存儲他的菜單項 ????ArrayList menuItems; ????public PancakeHouseMenu() { ????????menuItems = new ArrayList(); ????????// 在菜單的構(gòu)造器中,每一個菜單項都會被加入到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); ????} ????// 要加入一個菜單項,煎餅屋的做法是創(chuàng)建一個新的菜單項對象, ????// 傳入每一個變量,然后將它加入ArrayList中 ????public void addItem(String name, String description, ????????????????????????boolean vegetarian, double price) { ????????MenuItem menuItem = new MenuItem(name, description, vegetarian, price); ????????menuItems.add(menuItem); ????} ????// 這個方法返回菜單項列表 ????public ArrayList getMenuItems() { ????????return menuItems; ????} ????// 這里還有菜單的其他方法,這些方法都依賴于這個ArrayList,所以煎餅屋不希望重寫全部的代碼! ????// ... } // 餐廳的菜單實現(xiàn) public class DinnerMenu { ????// 餐廳采用使用的是數(shù)組,所以可以控制菜單的長度, ????// 并且在取出菜單項時,不需要轉(zhuǎn)型 ????static final int MAX_ITEMS = 6; ????int numberOfItems = 0; ????MenuItem[] menuItems; ????public DinnerMenu() { ????????menuItems = new MenuItem[MAX_ITEMS]; ????????// 和煎餅屋一樣,餐廳使用addItem()輔助方法在構(gòu)造器中創(chuàng)建菜單項的 ????????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, ????????????????????????boolean vegetarian, double price) { ????????// 餐廳堅持讓菜單保持在一定的長度之內(nèi) ????????MenuItem menuItem = new MenuItem(name, description, vegetarian, price); ????????if (numberOfItems >= MAX_ITEMS) { ????????????System.err.println("Sorry, menu is full! Can't add item to menu"); ????????} else { ????????????menuItems[numberOfItems] = menuItem; ????????????numberOfItems = numberOfItems + 1; ????????} ????} ????// getMenuItems()返回一個菜單項的數(shù)組 ????public MenuItem[] getMenuItems() { ????????return menuItems; ????} ????// 正如煎餅屋那樣,這里還有很多其他的菜單代碼依賴于這個數(shù)組 ????// ... } |
兩種不同的菜單表現(xiàn)方式,會帶來什么問題?
想了解為什么有兩種不同的菜單表現(xiàn)方式會讓事情變得復雜化,讓我們試著實現(xiàn)一個同時使用這兩個菜單的客戶代碼。假設(shè)你已經(jīng)被兩個餐廳合租的新公司雇用,你的工作是創(chuàng)建一個Java版本的女招待,她能應對顧客的需要打印定制的菜單,甚至告訴你是否某個菜單項是素食的,而無需詢問廚師。跟我們來看看這份關(guān)于女招待的規(guī)格,然后看看如何實現(xiàn)她。
Java版本的女招待規(guī)格:
| printMenu(): | 打印出菜單上的每一項 |
| printBreakfastMenu(): | 只打印早餐項 |
| printLunchMenu(): | 只打印午餐項 |
| printVegetarianMenu(): | 打印所有的素食菜單項 |
| isItemVegetarian(name): | 指定項的名稱,如果該項是素食,返回true,否則返回false |
我們先從實現(xiàn)printMenu()方法開始:
1.打印每份菜單上的所有項,必須調(diào)用PancakeHouseMenu和DinnerMenu的getMenuItems()方法,來取得它們各自的菜單項。請注意,兩者的返回類型是不一樣的。
| 1 2 3 4 5 6 7 | // getMenuItems()方法看起來是一樣的,但是調(diào)用所返回的結(jié)果卻是不一樣的類型。 // 早餐項是在一個ArrayList中,午餐項則是在一個數(shù)組中 PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu(); ArrayList breakfastItems = pancakeHouseMenu.getMenuItems(); DinnerMenu dinnerMenu = new DinnerMenu(); MenuItem[] lunchItems = dinnerMenu.getMenuItems(); |
2.現(xiàn)在,想要打印PancakeHouseMenu的項,我們用循環(huán)將早餐ArrayList內(nèi)的項一一列出來。想要打印DinnerMenu的項目,我們用循環(huán)將數(shù)組內(nèi)的項一一列出來。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 現(xiàn)在,我們必須實現(xiàn)兩個不同的循環(huán),個別處理這兩個不同的菜單 for (int i = 0; i < breakfastItems.size(); i++) { ????MenuItem menuItem = (MenuItem) breakfastItems.get(i); ????System.out.print(menuItem.getName() + " "); ????System.out.print(menuItem.getPrice() + " "); ????System.out.println(menuItem.getDescription() + " "); } for (int i = 0; i < lunchItems.length; i++) { ????MenuItem menuItem = lunchItems[i]; ????System.out.print(menuItem.getName() + " "); ????System.out.print(menuItem.getPrice() + " "); ????System.out.println(menuItem.getDescription() + " "); } |
3.實現(xiàn)女招待中的其他方法,做法也都和這里的方法相類似。我們總是需要處理兩個菜單,并且用兩個循環(huán)遍歷這些項。如果還有第三家餐廳以不同的實現(xiàn)出現(xiàn),我們就需要有三個循環(huán)。
下一步呢?
這兩個餐廳讓我們很為難,他們都不想改變自身的實現(xiàn),因為意味著要重寫許多代碼。但是如果他們其中一人不肯退讓,我們就很難辦了,我們所寫出來的女招待程序?qū)㈦y以維護、難以擴展。
如果我們能夠找到一個方法,讓他們的菜單實現(xiàn)一個相同的接口,該有多好!這樣一來,我們就可以最小化女招待代碼中的具體引用,同時還有希望擺脫遍歷這兩個菜單所需的多個循環(huán)。
聽起來很棒!但要怎么做呢?
如果你從本書中學到了一件事情,那就是封裝變化的部分。很明顯,在這里發(fā)生變化的是:由不同的集合(collection)類型所造成的遍歷。但是,這能夠被封裝嗎?讓我們來看看這個想法:
1.要遍歷早餐項,我們需要使用ArrayList的size()和get()方法:
| 1 2 3 | for (int i = 0; i < breakfastItems.size(); i++) { ????MenuItem menuItem = (MenuItem) breakfastItems.get(i); } |
2.要遍歷午餐項,我們需要使用數(shù)組的length字段和中括號:
| 1 2 3 | for (int i = 0; i < lunchItems.length; i++) { ????MenuItem menuItem = lunchItems[i]; } |
3.現(xiàn)在我們創(chuàng)建一個對象,把它稱為迭代器(Iterator),利用它來封裝“遍歷集合內(nèi)的每個對象的過程”。先讓我們在ArrayList上試試:
| 1 2 3 4 5 6 7 | // 我們從breakfastMenu中取得一個菜單項迭代器 Iterator iterator = breakfastMenu.createIterator(); // 當還有其他項時 while (iterator.hasNext()) { ????// 取得下一項 ????MenuItem menuItem = (MenuItem) iterator.next(); } |
4.將它也在數(shù)組上試試:
| 1 2 3 4 5 6 | // 這里的情況也是一樣的:客戶只需要調(diào)用hasNext()和next()即可, // 而迭代器會暗中使用數(shù)組的下標 Iterator iterator = lunchMenu.createIterator(); while (iterator.hasNext()) { ????MenuItem menuItem = (MenuItem) iterator.next(); } |
會見迭代器模式
看起來我們對遍歷的封裝已經(jīng)奏效了;你大概也已經(jīng)猜到,這正是一個設(shè)計模式,稱為迭代器模式。
關(guān)于迭代器模式,你所需要知道的第一件事情,就是它依賴于一個名為迭代器的接口。
| 1 2 3 4 5 6 | public interface Iterator { ????// hasNext()方法返回一個布爾值,讓我們知道是否還有更多的元素 ????boolean hasNext(); ????// next()方法返回下一個元素 ????Object next(); } |
現(xiàn)在,一旦我們有了這個接口,就可以為各種對象集合實現(xiàn)迭代器:數(shù)組、列表、散列表……
讓我們繼續(xù)實現(xiàn)這個迭代器,并將它掛鉤到DinnerMenu中,看它是如何工作的。
用迭代器改寫餐廳菜單
現(xiàn)在我們需要實現(xiàn)一個具體的迭代器,為餐廳菜單服務(wù):
| 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 | public class DinnerMenuIterator implements Iterator { ????MenuItem[] items; ????// position記錄當前數(shù)組遍歷的位置 ????int position = 0; ????// 構(gòu)造器需要被傳入一個菜單項的數(shù)組當做參數(shù) ????public DinnerMenuIterator(MenuItem[] items) { ????????this.items = items; ????} ????// next()方法返回數(shù)組內(nèi)的下一項,并遞增其位置 ????public Object next() { ????????MenuItem menuItem = items[position]; ????????position = position + 1; ????????return menuItem; ????} ????// hasNext()方法會檢查我們是否已經(jīng)取得數(shù)組內(nèi)所有的元素。 ????// 如果還有元素待遍歷,則返回true ????public boolean hasNext() { ????????if (position >= items.length || items[position] == null) { ????????????return false; ????????} else { ????????????return true; ????????} ????} } |
好了,我們已經(jīng)有了迭代器。現(xiàn)在就利用它來改寫餐廳菜單:我們只需要加入一個方法創(chuàng)建一個DinnerMenuIterator,并將它返回給客戶:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class DinnerMenu { ????static final int MAX_ITEMS = 6; ????int numberOfItems = 0; ????MenuItem[] menuItems; ????// ... ????// 我們不再需要getMenuItems()方法,事實上,我們根本不想要這個方法, ????// 因為它會暴露我們內(nèi)部的實現(xiàn)。 ????// 這是createIterator()方法,用來從菜單項數(shù)組創(chuàng)建一個DinnerMenuIterator, ????// 并將它返回給客戶 ????public Iterator createIterator() { ????????return new DinnerMenuIterator(menuItems); ????} ????// ... } |
現(xiàn)在將迭代器代碼整合進女招待中。
| 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 | public class Waitress { ????PancakeHouseMenu pancakeHouseMenu; ????DinnerMenu dinnerMenu; ????// 在構(gòu)造器中,女招待照顧兩個菜單 ????public Waitress(PancakeHouseMenu pancakeHouseMenu, DinnerMenu dinnerMenu) { ????????this.pancakeHouseMenu = pancakeHouseMenu; ????????this.dinnerMenu = dinnerMenu; ????} ????public void printMenu() { ????????// 這個printMenu()方法為每一個菜單各自創(chuàng)建一個迭代器 ????????Iterator pancakeIterator = pancakeHouseMenu.createIterator(); ????????Iterator dinnerIterator = dinnerMenu.createIterator(); ????????// 然后調(diào)用重載的printMenu(),將迭代器傳入 ????????printMenu(pancakeIterator); ????????printMenu(dinnerIterator); ????} ????// 這個重載的printMenu()方法,使用迭代器來遍歷菜單項并打印出來 ????private void printMenu(Iterator iterator) { ????????while (iterator.hasNext()) { ????????????MenuItem menuItem = (MenuItem) iterator.next(); ????????????System.out.println(menuItem.getName() + " " + ????????????????????menuItem.getPrice() + " " + menuItem.getDescription()); ????????} ????} } |
到目前為止,我們做了些什么?
首先,我們讓對象村的廚師們非常快樂。他們可以保持他們自己的實現(xiàn)又可以擺平差別。只要我們給他們這兩個迭代器(PancakeHouseMenuIterator和DinnerMenuIterator),他們只需要加入一個createIterator()方法,一切就大功告成了。
這個過程中,我們也幫了我們自己。女招待將會更容易維護和擴展。讓我們來徹底檢查一下到底我們做了哪些事,以及后果如何:
| 難以維護的女招待實現(xiàn) | 由迭代器支持的新女招待 |
| 菜單封裝得不好,餐廳使用的是ArrayList,而煎餅屋使用的是數(shù)組。 | 菜單的實現(xiàn)已經(jīng)被封裝起來了。女招待不知道菜單是如何存儲菜單項集合的。 |
| 需要兩個循環(huán)來遍歷菜單項。 | 只要實現(xiàn)迭代器,我們只需要一個循環(huán),就可以多態(tài)地處理任何項的集合。 |
| 女招待捆綁于具體類(MenuItem[]和ArrayList)。 | 女招待現(xiàn)在只使用一個接口(迭代器)。 |
| 女招待捆綁于兩個不同的具體菜單類,盡管這兩個類的接口大致上是一樣的。 | 現(xiàn)在的菜單接口完全一樣。但是,我們還是沒有一個共同的接口,也就是說女招待仍然捆綁于兩個具體的菜單類。這一點我們最好再修改一下。 |
做一些改良
好了,我們已經(jīng)知道這兩份菜單的接口完全一樣,但沒有為它們設(shè)計一個共同的接口。所以,接下來就要這么做,讓女招待更干凈一些。
Java有一個內(nèi)置的Iterator接口,讓我們先來看看:
| 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 | public interface Iterator<E> { ????/** ?????* Returns true if there is at least one more element, false otherwise. ?????* @see #next ?????*/ ????public boolean hasNext(); ????/** ?????* Returns the next object and advances the iterator. ?????* ?????* @return the next object. ?????* @throws NoSuchElementException ?????*???????????? if there are no more elements. ?????* @see #hasNext ?????*/ ????public E next(); ????/** ?????* Removes the last object returned by {@code next} from the collection. ?????* This method can only be called once between each call to {@code next}. ?????* ?????* @throws UnsupportedOperationException ?????*???????????? if removing is not supported by the collection being ?????*???????????? iterated. ?????* @throws IllegalStateException ?????*???????????? if {@code next} has not been called, or {@code remove} has ?????*???????????? already been called after the last call to {@code next}. ?????*/ ????public void remove(); } |
這個接口看起來和我們之前定義的一樣,只不過多了一個附加的方法,允許我們從聚合中刪除由next()方法返回的最后一項。
接下來讓我們用java.util.Iterator來清理代碼。
讓我們先從煎餅屋菜單開始,先把它改用java.util.Iterator,這很容易,只需要刪除煎餅屋菜單迭代器,然后在煎餅屋菜單的代碼前面加上 import java.util.Iterator。再改變下面這一行代碼就可以了:
| 1 2 3 | public Iterator createIterator() { ????return menuItems.iterator(); } |
這樣PancakeHouseMenu就完成了。
接著,我們處理DinnerMenu,以符合java.util.Iterator的需求。
| 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 | public class DinnerMenuIterator implements Iterator { ????MenuItem[] items; ????int position = 0; ????public DinnerMenuIterator(MenuItem[] items) { ????????this.items = items; ????} ????public Object next() { ????????MenuItem menuItem = items[position]; ????????position = position + 1; ????????return menuItem; ????} ????public boolean hasNext() { ????????if (position >= items.length || items[position] == null) { ????????????return false; ????????} else { ????????????return true; ????????} ????} ????// 我們需要實現(xiàn)remove()方法。因為使用的是固定長度的數(shù)組, ????// 所以在remove()方法被調(diào)用時,我們將后面的所有元素往前移動一個位置。 ????@Override ????public void remove() { ????????if (position <= 0) { ????????????throw new IllegalStateException("You can't remove ?????????????an item until you've done at least one next()"); ????????} ????????if (items[position - 1] != null) { ????????????for (int i = position-1; i < (items.length - 1); i++) { ????????????????items[i] = items[i + 1]; ????????????} ????????????items[items.length - 1] = null; ????????} ????} } |
我們只需要給菜單一個共同的接口,然后再稍微改一下女招待。這個Menu接口相當簡單:
| 1 2 3 | public interface Menu { ????public Iterator createIterator(); } |
現(xiàn)在,我們需要讓煎餅屋菜單類和餐廳菜單類都實現(xiàn)Menu接口,然后更新女招待的代碼:
| 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 { ????Menu pancakeHouseMenu; ????Menu dinnerMenu; ????// 將具體菜單類改成Menu接口 ????public Waitress(Menu pancakeHouseMenu, Menu dinnerMenu) { ????????this.pancakeHouseMenu = pancakeHouseMenu; ????????this.dinnerMenu = dinnerMenu; ????} ????// 以下的代碼沒有修改 ????public void printMenu() { ????????Iterator pancakeIterator = pancakeHouseMenu.createIterator(); ????????Iterator dinnerIterator = dinnerMenu.createIterator(); ????????printMenu(pancakeIterator); ????????printMenu(dinnerIterator); ????} ????private void printMenu(Iterator iterator) { ????????while (iterator.hasNext()) { ????????????MenuItem menuItem = (MenuItem) iterator.next(); ????????????System.out.println(menuItem.getName() + " " + ????????????????????menuItem.getPrice() + " " + menuItem.getDescription()); ????????} ????} } |
這為我們帶來了什么好處?煎餅屋菜單和餐廳菜單的類,都實現(xiàn)了Menu接口,女招待可以利用接口(而不是具體類)引用每一個菜單對象。這樣,通過“針對接口編程,而不針對實現(xiàn)編程”,我們就可以減少女招待和具體類之間的依賴。
定義迭代器模式
現(xiàn)在我們來看看這個模式的正式定義:
迭代器模式提供一種方法順序訪問一個聚合對象中的各個元素,而又不暴露其內(nèi)部的表示。 迭代器模式讓我們能游走于聚合內(nèi)的每一個元素,而又不暴露內(nèi)部的表示。把游走的任務(wù)放在迭代器上,而不是聚合上,這樣簡化了聚合的接口和實現(xiàn),也讓責任各得其所。
這很有意義:這個模式給你提供了一種方法,可以順序訪問一個聚集對象中的元素,而又不用知道內(nèi)部是如何表示的。你已經(jīng)在前面的兩個菜單實現(xiàn)中看到了這一點。在設(shè)計中使用迭代器的影響是明顯的:如果你有一個統(tǒng)一的方法訪問聚合中的每一個對象,你就可以編寫多態(tài)的代碼和這些聚合搭配使用,如同前面的printMenu()方法一樣,只要有了迭代器這個方法,根本不管菜單項究竟是由數(shù)組還是由ArrayList(或者其他能創(chuàng)建迭代器的東西)來保存的。
另一個對你的設(shè)計造成重要影響的,是迭代器模式把這些元素之間游走的責任交給迭代器,而不是聚合對象。這不僅讓聚合的接口和實現(xiàn)變得更簡潔,也可以讓聚合更專注在它所應該專注的事情上面(也就是管理對象組合),而不必去理會遍歷的事情。
單一責任
如果我們允許我們的聚合實現(xiàn)它們內(nèi)部的集合,以及相關(guān)的操作和遍歷的方法,又會如何?我們已經(jīng)知道這會增加聚合中的方法個數(shù),但又怎樣呢?為什么這么做不好?
想知道為什么,首先你需要認清楚,當我們允許一個類不但要完成自己的事情(管理某種聚合),還同時要擔負更多的責任(例如遍歷)時,我們就給了這個類兩個變化的原因。兩個?沒錯,就是兩個!如果這個集合改變的話,這個類也必須改變,如果我們遍歷的方式改變的話,這個類也必須跟著改變。所以,再一次地,我們的老朋友“改變”又成了我們設(shè)計原則的中心:
設(shè)計原則:一個類應該只有一個引起變化的原因
我們知道要避免類內(nèi)的改變,因為修改代碼很容易造成許多潛在的錯誤。如果有一個類具有兩個改變的原因,那么這會使得將來該類的變化幾率上升,而當它真的改變時,你的設(shè)計中同時有兩個方面將會受到影響。
沒錯,這聽起來很容易,但其實做起來并不簡單:區(qū)分設(shè)計中的責任,是最困難的事情之一。我們的大腦很習慣看著一大群的行為,然后將它們集中在一起,盡管他們可能屬于兩個或者多個不同的責任。想要成功的唯一方法,就是努力不懈地檢查你的設(shè)計,隨著系統(tǒng)的成長,隨時觀察有沒有跡象顯示某個類改變的原因超出一個。
原文出處:?cashow
from:?http://www.importnew.com/23462.html
總結(jié)
以上是生活随笔為你收集整理的《Head first设计模式》学习笔记 – 迭代器模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 帝国之消息队列
- 下一篇: Java线程池框架核心代码分析