Css3圖片陰影效果_Css3相簿陰影效果(二)
迭代器模式提供一種方法順序訪問一個聚合物件中的各個元素,而又不暴露其內部的表示。
新的餐廳想用煎餅屋選單當作早餐的選單,使用餐廳的選單當做午餐的選單。煎餅屋使用ArrayList記錄他的選單項,而餐廳使用的是陣列。想要知道選單列表的話就需要知道具體型別。
檢查選單項
讓我們先檢查每份選單上的專案和實現。
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; } }
兩個餐廳的選單實現
我們先來看看兩個餐廳的選單實現
1. 這是煎餅屋的選單實現 public class PancakeHouseMenu { // 煎餅屋使用一個ArrayList儲存他的選單項 ArrayList menuItems; public PancakeHouseMenu() { menuItems = new ArrayList(); // 在選單的構造器中,每一個選單項都會被加入到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); } // 要加入一個選單項,煎餅屋的做法是建立一個新的選單項物件, // 傳入每一個變數,然後將它加入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,所以煎餅屋不希望重寫全部的程式碼! // ... } 2. 餐廳的選單實現 public class DinnerMenu { // 餐廳採用使用的是陣列,所以可以控制選單的長度, // 並且在取出選單項時,不需要轉型 static final int MAX_ITEMS = 6; int numberOfItems = 0; MenuItem[] menuItems; public DinnerMenu() { menuItems = new MenuItem[MAX_ITEMS]; // 和煎餅屋一樣,餐廳使用addItem()輔助方法在構造器中建立選單項的 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) { // 餐廳堅持讓選單保持在一定的長度之內 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()返回一個選單項的陣列 public MenuItem[] getMenuItems() { return menuItems; } // 正如煎餅屋那樣,這裡還有很多其他的選單程式碼依賴於這個陣列 // ... }
兩種選單表現方式帶來的問題
建立一個Java版本的女招待,她能應對顧客的需要列印定製的選單。
Java版本女招待規格
- printMenu(): 打印出選單上的每一項
- printBreakfastMenu(): 只打印早餐項
- printLunchMenu(): 只打印午餐項
- printVegetarianMenu(): 列印所有的素食選單項
- isItemVegetarian(name): 指定項的名稱,如果該項是素食,返回true,否則返回false
我們先從實現printMenu()方法開始:
1.列印每份選單上的所有項,必須呼叫PancakeHouseMenu和DinnerMenu的getMenuItems()方法,來取得它們各自的選單項。請注意,兩者的返回型別是不一樣的。
// getMenuItems()方法看起來是一樣的,但是呼叫所返回的結果卻是不一樣的型別。
// 早餐項是在一個ArrayList中,午餐項則是在一個數組中
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
ArrayList breakfastItems = pancakeHouseMenu.getMenuItems();
DinnerMenu dinnerMenu = new DinnerMenu();
MenuItem[] lunchItems = dinnerMenu.getMenuItems();
2.現在,想要列印PancakeHouseMenu的項,我們用迴圈將早餐ArrayList內的項一一列出來。想要列印DinnerMenu的專案,我們用迴圈將陣列內的項一一列出來。
// 現在,我們必須實現兩個不同的迴圈,個別處理這兩個不同的選單
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.實現女招待中的其他方法,做法也都和這裡的方法相類似。我們總是需要處理兩個選單,並且用兩個迴圈遍歷這些項。如果還有第三家餐廳以不同的實現出現,我們就需要有三個迴圈。
下一步呢?
我們所寫出來的女招待程式將難以維護、難以擴充套件。
如果我們能夠找到一個方法,讓他們的選單實現一個相同的介面,該有多好!這樣一來,我們就可以最小化女招待程式碼中的具體引用,同時還有希望擺脫遍歷這兩個選單所需的多個迴圈。
聽起來很棒!但要怎麼做呢?
如果你從本書中學到了一件事情,那就是封裝變化的部分。很明顯,在這裡發生變化的是:由不同的集合(collection)型別所造成的遍歷。但是,這能夠被封裝嗎?讓我們來看看這個想法:
1.要遍歷早餐項,我們需要使用ArrayList的size()和get()方法:
for (int i = 0; i < breakfastItems.size(); i++) {
MenuItem menuItem = (MenuItem) breakfastItems.get(i);
}
2.要遍歷午餐項,我們需要使用陣列的length欄位和中括號:
for (int i = 0; i < lunchItems.length; i++) {
MenuItem menuItem = lunchItems[i];
}
3.現在我們建立一個物件,把它稱為迭代器(Iterator),利用它來封裝“遍歷集合內的每個物件的過程”。先讓我們在ArrayList上試試:
// 我們從breakfastMenu中取得一個選單項迭代器
Iterator iterator = breakfastMenu.createIterator();
// 當還有其他項時
while (iterator.hasNext()) {
// 取得下一項
MenuItem menuItem = (MenuItem) iterator.next();
}
4.將它也在陣列上試試:
// 這裡的情況也是一樣的:客戶只需要呼叫hasNext()和next()即可,
// 而迭代器會暗中使用陣列的下標
Iterator iterator = lunchMenu.createIterator();
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
}
會見迭代器模式
關於迭代器模式,你所需要知道的第一件事情,就是它依賴於一個名為迭代器的介面。
public interface Iterator {
// hasNext()方法返回一個布林值,讓我們知道是否還有更多的元素
boolean hasNext();
// next()方法返回下一個元素
Object next();
}
現在,一旦我們有了這個介面,就可以為各種物件集合實現迭代器:陣列、列表、散列表……
讓我們繼續實現這個迭代器,並將它掛鉤到DinnerMenu中,看它是如何工作的。
用迭代器改寫餐廳選單
現在我們需要實現一個具體的迭代器,為餐廳選單服務:
public class DinnerMenuIterator implements Iterator {
MenuItem[] items;
// position記錄當前陣列遍歷的位置
int position = 0;
// 構造器需要被傳入一個選單項的陣列當做引數
public DinnerMenuIterator(MenuItem[] items) {
this.items = items;
}
// next()方法返回陣列內的下一項,並遞增其位置
public Object next() {
MenuItem menuItem = items[position];
position = position + 1;
return menuItem;
}
// hasNext()方法會檢查我們是否已經取得陣列內所有的元素。
// 如果還有元素待遍歷,則返回true
public boolean hasNext() {
if (position >= items.length || items[position] == null) {
return false;
} else {
return true;
}
}
}
好了,我們已經有了迭代器。現在就利用它來改寫餐廳選單:我們只需要加入一個方法建立一個DinnerMenuIterator,並將它返回給客戶:
public class DinnerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
// ...
// 我們不再需要getMenuItems()方法,事實上,我們根本不想要這個方法,
// 因為它會暴露我們內部的實現。
// 這是createIterator()方法,用來從選單項陣列建立一個DinnerMenuIterator,
// 並將它返回給客戶
public Iterator createIterator() {
return new DinnerMenuIterator(menuItems);
}
// ...
}
現在將迭代器程式碼整合進女招待中。
public class Waitress {
PancakeHouseMenu pancakeHouseMenu;
DinnerMenu dinnerMenu;
// 在構造器中,女招待照顧兩個選單
public Waitress(PancakeHouseMenu pancakeHouseMenu, DinnerMenu dinnerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinnerMenu = dinnerMenu;
}
public void printMenu() {
// 這個printMenu()方法為每一個選單各自建立一個迭代器
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinnerIterator = dinnerMenu.createIterator();
// 然後呼叫過載的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());
}
}
}
到目前為止,我們做了些什麼?
只要我們給他們兩個迭代器(PancakeHouseMenuIterator和DinnerMenuIterator),他們只需要加入一個createIterator()方法,一切就大功告成了。
難以維護的女招待實現 | 由迭代器支援的新女招待 |
---|---|
選單封裝得不好,餐廳使用的是ArrayList,而煎餅屋使用的是陣列。 | 選單的實現已經被封裝起來了。女招待不知道選單是如何儲存選單項集合的。 |
需要兩個迴圈來遍歷選單項。 | 只要實現迭代器,我們只需要一個迴圈,就可以多型地處理任何項的集合。 |
女招待捆綁於具體類(MenuItem[]和ArrayList)。 | 女招待現在只使用一個介面(迭代器)。 |
女招待捆綁於兩個不同的具體選單類,儘管這兩個類的介面大致上是一樣的。 | 現在的選單介面完全一樣。但是,我們還是沒有一個共同的介面,也就是說女招待仍然捆綁於兩個具體的選單類。這一點我們最好再修改一下。 |
做一些改良
好了,我們已經知道這兩份選單的介面完全一樣,但沒有為它們設計一個共同的介面。所以,接下來就要這麼做,讓女招待更乾淨一些。
Java有一個內建的Iterator介面,讓我們先來看看:
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();
用java.util.Iterator來清理程式碼
- 煎餅屋選單修改:
public Iterator createIterator() {
return menuItems.iterator();
}
- 修改DinnerMenu
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;
}
}
// 我們需要實現remove()方法。因為使用的是固定長度的陣列,
// 所以在remove()方法被呼叫時,我們將後面的所有元素往前移動一個位置。
@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;
}
}
}
我們只需要給選單一個共同的介面createIterator,讓煎餅屋選單類和餐廳選單類都實現Menu介面,然後再稍微改一下女招待。
// Menu介面
public interface Menu {
public Iterator createIterator();
}
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());
}
}
這為我們帶來了什麼好處?煎餅屋選單和餐廳選單的類,都實現了Menu介面,女招待可以利用介面(而不是具體類)引用每一個選單物件。這樣,通過“針對介面程式設計,而不針對實現程式設計”,我們就可以減少女招待和具體類之間的依賴。
定義迭代器模式
迭代器模式提供一種方法順序訪問一個聚合物件中的各個元素,而又不暴露其內部的表示。
如果你有一個統一的方法訪問聚合中的每一個物件,你就可以編寫多型的程式碼和這些聚合搭配使用,如同前面的printMenu()方法一樣,只要有了迭代器這個方法,根本不管選單項究竟是由陣列還是由ArrayList(或者其他能建立迭代器的東西)來儲存的。
另一個對你的設計造成重要影響的,是迭代器模式把這些元素之間遊走的責任交給迭代器,而不是聚合物件。這不僅讓聚合的介面和實現變得更簡潔,也可以讓聚合更專注在它所應該專注的事情上面(也就是管理物件組合),而不必去理會遍歷的事情。
改進前:
改進後:
迭代器模式類圖
單一責任
如果我們允許我們的聚合實現它們內部的集合,以及相關的操作和遍歷的方法,又會如何?我們已經知道這會增加聚合中的方法個數,但又怎樣呢?為什麼這麼做不好?
想知道為什麼,首先你需要認清楚,當我們允許一個類不但要完成自己的事情(管理某種聚合),還同時要擔負更多的責任(例如遍歷)時,我們就給了這個類兩個變化的原因。兩個?沒錯,就是兩個!如果這個集合改變的話,這個類也必須改變,如果我們遍歷的方式改變的話,這個類也必須跟著改變。所以,再一次地,我們的老朋友“改變”又成了我們設計原則的中心:
設計原則:一個類應該只有一個引起變化的原因
我們知道要避免類內的改變,因為修改程式碼很容易造成許多潛在的錯誤。如果有一個類具有兩個改變的原因,那麼這會使得將來該類的變化機率上升,而當它真的改變時,你的設計中同時有兩個方面將會受到影響。
要如何解決呢?這個原則告訴我們將一個責任只指派給一個類。
內聚(cohesion):用來度量一個類或模組緊密地達到單一目的或責任。
當一個模組或一個類被設計成只支援一組相關的功能時,我們說它具有高內聚;反之,當被設計成支援一組不相關的功能時,我們說它具有低內聚。
內聚是一個比單一責任原則更普遍的概念,但兩者其實關係是很密切的。遵守這個原則的類容易具有很高的凝聚力,而且比揹負許多責任的低內聚類更容易維護。
要點
- 迭代器允許訪問聚合的元素,而不需要暴露它的內部結構。
- 迭代器將遍歷聚合的工作封裝進一個物件中。當使用迭代器的時候,我們依賴聚合提供遍歷。迭代器提供了一個通用的介面,讓我們遍歷聚合的項,當我們編碼使用聚合的項時,就可以使用多型機制。
- 我們應該努力讓一個類只分配一個責任。