遍歷“容器”的優雅方法——總結迭代器模式


前言

本文主要是讀書筆記的整理,自己總結的倒不多,做個記錄

聚集(集合)的概念

如果能把多個普通類的對象聚在一起形成一個總體,這個總體就被稱之為聚集(Aggregate),舉例子:

1、在任何編程語言中:數組都是最基本的聚集,在Java中,數組也是其他的 JAVA 聚集對象的設計基礎。

2、在Java里,JAVA聚集對象都是實現了 java.util.Collection 接口的對象,是 JAVA 對聚集概念的直接支持。從 JDK 1.2 開始,JAVA 提供了多種現成的聚集 API,包括 Vector、ArrayList、HashSet、HashMap、Hashtable、ConcurrentHashMap 等。

自定義容器的封閉需求

假如因業務需要,RD 定義了專屬的數據元素的聚集,還要把它提供給客戶端,讓其調用(不特別強調,也包括其他依賴服務)。但是有時候為了安全,RD 不想讓客戶端看到聚集的內部實現,只是能讓她們訪問就可以了,比如遍歷等操作。還有的時候,客戶端不需要了解具體實現,能否讓客戶端跳開復雜的數據結構?因為調用者們不需要了解實現方式,只要能開箱即用即可。

為了解決這個問題,那么就需要有一種策略能讓客戶端遍歷這個聚集體的時候,無法窺破RD存儲對象的方式,無需了解內部的復雜數據結構。

迭代器的引出——茶餐廳和煎餅鋪子合並的經典案例

有兩個遺留的點餐系統,包括一套餐廳點餐系統——專門提供正餐,和一個煎餅鋪子點餐系統(不要糾結為啥煎餅攤也有點餐系統。。。)——專門提供早餐(除了早餐,其他時間不開放)。

現狀

餐廳里有很多賣飯的窗口,它們的業務是一塊單獨的實現,隔壁煎餅鋪的業務,也是一塊單獨的實現。現在有個老板想把它們收購並合並,讓客戶能在一個地方,一個時間段內,同時吃煎餅和餐廳的各種菜。目前餐廳內有至少兩家餐館都統一實現了 MenuItem 類——菜單子系統的菜單類。

問題

但是煎餅的菜單系統用的 ArrayList 記錄菜單,而餐廳的 RD 用的是數組實現了菜單系統,雙方的RD,都不願意花費時間修改自己的實現。畢竟有很多其他服務依賴了菜單子系統,如下 MenuItem 代碼:

/**
 * 餐廳的菜單都是午餐項目,煎餅的菜單,都是早餐項目,但是它們都屬於菜單,即:
 * 都有菜品名稱,描述,是否是素的,價格等
 * 故設計這樣一個類作為菜單項目類
 */
public class MenuItem {
    String name;
    String description;
    public MenuItem(String name,
                    String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }
}

不同的餐廳使用了這個 MenuItem 類

/**
 * 煎餅窗口的菜單
 */
public class PancakeHouseMenu {
    private List<MenuItem> menuItems; // menuItems 使用 ArrayList 存儲菜單的項目,動態數組,使其很容易擴大菜單規模

    /**
     * 在構造菜單的時候,把菜單加入到 ArrayList menuItems
     */
    public PancakeHouseMenu() {
        menuItems = new ArrayList<>();
        addItem("K&B's Pancake Breakfast",
                "Pancakes with scrambled eggs, and toast");

        addItem("Regular Pancake Breakfast",
                "Pancakes with fried eggs, sausage");
    }

    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description);
        menuItems.add(menuItem);
    }

    public List<MenuItem> getMenuItems() {
        return menuItems;
    }
}
 
///////////////////////////////////////////////////////////////
/**
 * 餐廳的菜單
 */
public class DinerMenu {
    private static final int MAX_ITEMS = 6;
    private int numberOfItems = 0;
    private MenuItem[] menuItems; // 使用了真正的數組實現菜單項的存儲

    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];
        addItem("Vegetarian BLT", "(Fakin') Bacon with lettuce & tomato on whole wheat");
        addItem("BLT", "Bacon with lettuce & tomato on whole wheat");
        addItem("Soup of the day", "Soup of the day, with a side of potato salad");
    }

    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description);
        if (numberOfItems >= MAX_ITEMS) {
            System.err.println("Sorry, menu is full!  Can't add item to menu");
        } else {
            menuItems[numberOfItems] = menuItem;
            numberOfItems = numberOfItems + 1;
        }
    }

    public MenuItem[] getMenuItems() {
        return menuItems;
    }
}

兩種不同的菜單表現方式,會給客戶端調用帶來很多問題,假設客戶端是服務員類——Waitress,下面是客戶端的業務:

  • 打印出菜單上的每一項:打印每份菜單上的所有項,必須調用 PancakeHouseMenu 和 DinerMenu 的 getMenuItem 方法,來取得它們各自的菜單項,但是兩者返回類型是不一樣的

  • 只打印早餐項(PancakeHouseMenu 的菜單)或者只打印午餐項(DinerMenu 的菜單)

    • 想要打印 PancakeHouseMenu 的項,我們用循環將早餐 ArrayList 內的項列出來

    • 想要打印 DinerMenu 的項目,我們用循環將數組內的項一一列出來

  • 打印所有的素食菜單項

  • 指定項的名稱,如果該項是素食的話,返回true,否則返回false

實現 Waitress 的其他方法,做法都和上面的方法類似,發現 Waitress 處理兩個菜單時,總是需要寫兩個形式相似的循環,去遍歷這些菜單,而且一旦外部菜單的數據結構變了,客戶端也得跟着修改。

再有,如果還有第三家餐廳合並,而且坑爹的是,它以完全不同的實現方式實現了菜單……那怎么辦?此時難道還繼續寫第三個循環么……

以后,這樣甚至能發展到 N 個不同形式的循環……

這顯然是非常不好的設計,直接導致后期系統的大量垃圾代碼和日益艱巨的維護任務。

為什么出現這種結局?

封裝特性

面向接口編程

代碼冗余

Waitress (也就是客戶端)竟然能非常清晰的,

而且是必須清晰的熟悉服務端的實現,這是很不科學的

PancakeHouseMenu 和 DinerMenu 都沒有面向接口編程,

而直接實現了具體業務,導致擴展困難

DinerMenu和PancakeHouseMenu都有很大重復代碼,

沒有抽象共享

那么可以解決么?

解決方法

1、Waitress 要遍歷早餐項,需要使用 ArrayList 的 size() 和 get() 方法

2、Waitress 遍歷午餐項,需要使用數組的 length 字段和中括號

現在創建一個新的對象,將它稱為迭代器(Iterator),利用它來封裝“遍歷集合內的每個對象的過程”,下面對其抽象、封裝。

原則:只封裝變化的部分

案例中變化的部分:因為不同的集合實現,導致的不同的遍歷方式。將其封裝即可,其實,這正是迭代器模式的應用。迭代器 Iterator,是面向接口編程,故它依賴於一個稱為迭代器的接口:

/**
 * 迭代器的接口,一旦有了這個接口,就可以為給種對象集合實現迭代器:數組、列表、散列表等等
 */
public interface Iterator {
    /**
     * 聚集中,是否還有元素
     */
    boolean hasNext();

    /**
     * 返回聚集中的下一個元素
     */
    Object next();
}

讓餐廳實現迭代器接口 —— Iterator,打造一個餐廳菜單迭代器——DinerMenuIterator

/**
 * 餐廳的迭代器
 */
public class DinerMenuIterator implements Iterator {
    private MenuItem[] items;
    private int position = 0;

    public DinerMenuIterator(MenuItem[] items) {
        this.items = items;
    }

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

    public boolean hasNext() {
        return position < items.length && items[position] != null;
    }
}

改造具體餐廳的菜單舊實現,把之前的如下代碼刪掉,因為它會暴露餐廳菜單的內部數據結構 menuItems

public MenuItem[] getMenuItems() {
    return menuItems;
}

下面是改造之后的餐廳菜單實現,PancakeHouseMenu 實現類似。

public class DinerMenu {
    private static final int MAX_ITEMS = 6;
    private int numberOfItems = 0;
    private MenuItem[] menuItems;

    // 實現方式不變
    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];
        addItem("Vegetarian BLT", "(Fakin') Bacon with lettuce & tomato on whole wheat");
        addItem("BLT", "Bacon with lettuce & tomato on whole wheat");
        addItem("Soup of the day", "Soup of the day, with a side of potato salad");
    }

    // 實現方式不變
    public void addItem(String name, String description) {
        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 方法,因為它會暴露內部實現,返回的直接是菜單的數據結構
    // 這個新方法代替 getMenuItems,createIterator 返回的是迭代器接口
    public Iterator createIterator() {
        return new DinerMenuIterator(menuItems);
    }
}

這樣寫客戶端的代碼就不會重復兩遍,如下,把迭代器的代碼整合到 Waitress,改掉之前冗余的循環遍歷代碼,只需要傳入一個迭代器作為遍歷方法的參數,把遍歷聚集的工作,委托給迭代器實現。既能保護內部實現,也能抽象遍歷形式,精簡代碼。也符合了開閉原則——以后菜單的實現邏輯修改了,客戶端也不用修改調用的代碼。

public class Waitress {
    // 服務員依賴的菜單系統
    private PancakeHouseMenu pancakeHouseMenu;
    private DinerMenu dinerMenu;

    public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinerMenu = dinerMenu;
    }

    /**
     * 遍歷全部菜單,無需在客戶端里積壓多個重復的循環代碼,也符合了開閉原則——以后修改遍歷邏輯,客戶端不需要修改
     */
    public void printMenu() {
        // 為每個菜單系統,創建一個迭代器 Iterator
        Iterator pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator dinerIterator = dinerMenu.createIterator();// 把迭代器子類型,傳入
        printMenu(pancakeIterator);// 把迭代器子類型,傳入
        printMenu(dinerIterator);
    }

    /**
     * 接口的用法,向上轉型
     */
    private void printMenu(Iterator iterator) {
        // 先判斷是否還能繼續迭代
        while (iterator.hasNext()) {
            // Iterator 接口里 next 返回的是 Object 對象,故需要強制轉換
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.print(menuItem.getName() + ", ");
            System.out.println(menuItem.getDescription());
        }
    }
}
 
//////
public class MenuTestDrive {
    public static void main(String args[]) {
        PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
        DinerMenu dinerMenu = new DinerMenu();
        Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
        waitress.printMenu();
    }
}

到底解決了什么問題

經迭代器模式對菜單系統進行封裝,使得各個餐廳的菜單系統能維持不變,磨平了實現的差別,減少了重寫的工作量。

舊版代碼的客戶端

基於迭代器模式封裝服務后,重寫的客戶端

遍歷:需要多個代碼重復度較高的循環來實現,代碼冗余度很高,加大無意義的工作量

只需要增加類,去實現各個菜單系統的迭代器,客戶端只需要一個循環就能搞定所有的菜單服務調用

各個菜單系統的具體實現,封裝的不行,對客戶端暴露了數據結構,這是沒有任何必要的

菜單的具體實現被封裝,對外只公開迭代器,客戶端不知道,也不需要知道具體菜單的實現

客戶端被捆綁到了多個菜單實現類,牽一發動全身

客戶端可以只用 iterator 接口做參數,通過向上轉型,擺脫多個具體實現的捆綁,實現解耦

繼續發現問題

客戶端 Waitress 組合了多個具體實現類,仍然會牽一發動全身,比如修改了菜單的類名,客戶端就失效,也需要修改,仍然重度依賴

而且,具體菜單的實現類又有共同的方法 createIterator ,完全可以進一步抽象。

改進上述設計——充分利用 JDK 自帶的迭代器

首先不再為List這樣的數據結構重新實現迭代器,因為JDK 5 之后,Java 已經給我們實現好了,對於JDK 5 之后的所有集合容器,都可以采用 JDK 自帶的迭代器接口——java.util,Itreator,所以我們就不用自己寫,只需實現數組的迭代器即可。

1、記住:JDK 不支持為數組生成迭代器

2、java.util 包中的 Collection 接口——Java 所有的集合都實現了該接口,該接口有迭代器方法。 

繼續改進——抽象具體類的公共部分

可以為各個菜單實現類,提供一個公共的接口——Menu

原則:面向接口編程

有多個具體實現類的時候,要首先考慮不針對實現編程,而是面向接口編程,除非有共同的抽象方法+屬性時,可以考慮抽象父類。本案例中,只需使用接口,就可以減少客戶端 waittress 和具體菜單實現類之間的依賴。

import java.util.Iterator;

/**
 * 菜單系統要實現的方法,抽象為接口
 */
public interface Menu {
    Iterator createIterator();
}
 
/////////////////////////////
/**
 * 菜單的每項,抽象為類
 */
public class MenuItem {
    private String name;
    private String description;public MenuItem(String name,
                    String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }
}
 
/////////////////////////////////
// 餐廳菜單系統的迭代器,不需要實現額外聲明的迭代器接口,而是重寫JDK的迭代器即可
import java.util.Iterator;

/**
 * 重寫 JDK 的迭代器
 * implements java.util.Iterator;
 */
public class DinerMenuIterator implements Iterator {
    private MenuItem[] items;
    private int position = 0;

    public DinerMenuIterator(MenuItem[] items) {
        this.items = items;
    }

    // 不需要變
    @Override
    public Object next() {
        MenuItem menuItem = items[position];
        position = position + 1;
        return menuItem;
    }

    // 不需要變
    @Override
    public boolean hasNext() {
        return position < items.length && items[position] != null;
    }

    // 重新實現,最好是重寫
    @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;
        }
    }
}
 
////////////////////
// 餐廳菜單系統
import java.util.Iterator;

/**
 * Created by wangyishuai on 2018/1/27
 */
public class DinerMenu implements Menu {
    private static final int MAX_ITEMS = 6;
    private int numberOfItems = 0;
    private MenuItem[] menuItems;

    // 實現方式不變
    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];
        addItem("Soup of the day",
                "Soup of the day, with a side of potato salad");
    }

    // 實現方式不變
    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;
        }
    }

    // 返回的是 java.util.Iterator;
    @Override
    public Iterator createIterator() {
        return new DinerMenuIterator(menuItems);
    }
}
 
/////////////////////////////////////////
// 煎餅,不需要再實現迭代器,因為使用的數據結構是JDK的容器,而對於JDK自帶的集合容器,不需要自己實現迭代器
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 對於JDK的集合容器——List,不需要RD實現迭代器
 */
public class PancakeHouseMenu implements Menu {
    private List<MenuItem> menuItems;

    public PancakeHouseMenu() {
        menuItems = new ArrayList<>();
        addItem("K&B's Pancake Breakfast", "Pancakes with scrambled eggs, and toast");
    }

    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description);
        menuItems.add(menuItem);
    }

    @Override
    public Iterator createIterator() {
        // 返回 JDK ArrayList 自帶的迭代器 iterator() 方法
        return menuItems.iterator();
    }
}
 
//////////////////////////////// 客戶端
import java.util.Iterator;

public class Waitress {
    // 服務員依賴的菜單系統——通過接口解耦合
    private Menu pancakeHouseMenu;
    private Menu dinerMenu;

    // 修改為 Menu 接口,向上轉型,解耦合
    public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinerMenu = dinerMenu;
    }

    /**
     * 以后修改遍歷邏輯,客戶端不需要修改
     * // 不用修改
     */
    public void printMenu() {
        // 為每個菜單系統,創建迭代器
        // java.util.Iterator;
        Iterator pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator dinerIterator = dinerMenu.createIterator();
        printMenu(pancakeIterator);
        printMenu(dinerIterator);
    }

    /**
     * 接口的用法,向上轉型
     * // 不用修改
     * java.util.Iterator;
     */
    private void printMenu(Iterator iterator) {
        // 先判斷是否還能繼續迭代
        while (iterator.hasNext()) {
            // Iterator 接口里 next 返回的是 Object 對象,故需要強制轉換
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.print(menuItem.getName() + ", ");
            System.out.println(menuItem.getDescription());
        }
    }
}
public class MenuTestDrive {
    public static void main(String args[]) {
        Menu pancakeHouseMenu = new PancakeHouseMenu();
        Menu dinerMenu = new DinerMenu();
        // 即使具體的菜單實現類修改了名字或者環了實現類,客戶端——Waitress 也不需要修改代碼,解了耦合
        Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
        waitress.printMenu();
    }
}

針對 JDK 的迭代器重寫的原則

remove 方法應不應該重寫

雖然對於客戶端來說,remove 方法非必須(當然業務需要的話,就必須自定義重寫 remove),但是最好還是提供該方法,因為JDK的 Iterator接口里包含了該方法,如果不一起重寫,可能會出問題。

如果客戶端真的不需要刪除元素,那么最好也重寫該方法,只需要在重寫的時候拋出一個自定義的(或者現成的)異常——如果有調用,就提醒客戶端不能刪除元素。JDK也是這樣設計的,默認拋出異常 UnsupportedOperationException

線程安全問題

默認的迭代器接口是線程不安全的,如果有需要,要額外的加強線程安全。

迭代器模式的標准概念

迭代器模式又叫游標(Cursor)模式、Iterator模式,迭代子模式……是對象的行為模式之一,它把對容器中包含的內部對象的訪問委托給外部的類,讓外部的類可以使用 Iterator 按順序進行遍歷訪問,而又不暴露其內部的數據結構。

Iterator Pattern (Another Name: Cursor)

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

脫離Java的領域,那么可以認為:迭代器模式可以順序地訪問聚集中的元素,而不必暴露聚集的內部狀態(internal representation)。它把遍歷的責任轉移到了迭代器,而不是聚集本身,簡化了聚集的接口和實現代碼,也分割了責任。

迭代器模式的角色

Iterator(迭代器接口):該接口必須定義實現迭代功能的最小定義方法集,比如提供hasNext()和next()方法。

ConcreteIterator(具體的迭代器實現類):迭代器接口Iterator的實現類。可以根據具體情況加以實現。

Aggregate(聚集的接口):定義基本功能以及提供類似Iterator iterator()的方法。

concreteAggregate(聚集接口的實現類):容器接口的實現類。必須實現生成迭代器的方法。 

聚集體如果不使用 Iterator 模式,會存在什么問題

聚集類承擔了太多功能

如果是自定義的聚集,那么需要由聚集自己實現順序遍歷的方法——直接在聚集的類里添加遍歷方法。這樣,容器類承擔了太多功能:

一方面需要提供添加、刪除等本身應有的功能;

一方面還需要提供遍歷訪問功能。

不僅責任不分離,還和客戶端耦合太強

暴露聚集的太多內部實現細節

如果不使用迭代器模式,那么需要客戶端自己實現服務的遍歷(聯系餐廳和煎餅屋的合並案例),會直接暴露聚集的數據結構,往往這是不必要的,客戶端不需要了解服務的具體實現,也是為了程序的安全——不暴露太多的內部細節給客戶端。

遍歷聚集的時候修改聚集的元素,引起聚集的狀態混亂

如果使用的是 JDK 的集合類,如果直接遍歷,且遍歷的時候對集合修改,會有異常拋出。因為,往往容器在實現遍歷的過程中,需要保存遍歷狀態,當遍歷操作和元素的添加、刪除等操作夾雜在一起,這些更新功能在遍歷的時候也被調用,很容易引起集合的狀態混亂和程序運行錯誤等。此時應該為聚集使用迭代器模式,如果是JDK的集合類,就直接使用自帶的迭代器進行迭代。

記住:Java 中的 foreach 循環看起來像一個迭代器,但實際上並不是,還是要使用迭代器模式

Iterator 支持從源集合中安全地刪除對象,只需在 Iterator 上調用 remove() 即可。這樣做的好處是可以避免 ConcurrentModifiedException ,這個異常顧名思意:當打開 Iterator 迭代集合時,同時又在對集合進行修改。有些集合不允許在迭代時刪除或添加元素,但是調用 Iterator 的remove() 方法是個安全的做法。

List<String> list = new ArrayList<>(Arrays.asList("a","b","c","d"));
for(String s : list){
    if(s.equals("a")){
        list.remove(s);
    }
}
 
//會拋出一個ConcurrentModificationException異常,相反下面的顯示正常
List<String> list = new ArrayList<>(Arrays.asList("a","b","c","d"));
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
        String s = iter.next();
        if(s.equals("a")){
            iter.remove();
    }
}
// next() 必須在 remove() 之前調用。
// 在 foreach 中,編譯器會使 next() 在刪除元素之后被調用,因此就會拋出 ConcurrentModificationException 異常

參考

1、Iterator的remove方法可保證從源集合中安全地刪除對象(轉)

2、正確遍歷刪除List中的元素方法  http://www.jb51.net/article/98763.htm

Fail Fast 問題

如果一個算法開始之后,它的運算環境發生變化,使得算法無法進行必需的調整時,這個算法就應當立即發出故障信號。這就是 Fail Fast 的含義。同理,如果聚集的元素在一個動態迭代子的迭代過程中發生變化,迭代過程會受到影響而變得不能自恰。這時候,迭代子就應立即拋出一個異常。這種迭代子就是實現了Fail Fast 功能的迭代子。

使用迭代器模式的優點

Iterator 模式就是為了有效地處理按順序進行遍歷訪問的一種設計模式,簡單地說,Iterator模式提供一種有效的方法,可以屏蔽聚集對象的容器類的實現細節,而能對容器內包含的對象元素按順序進行有效的遍歷訪問。所以,Iterator模式的應用場景可以歸納為以下幾個:

  • 訪問容器中包含的內部對象

  • 按順序訪問

優點總結:

1,實現功能分離,簡化聚集的接口。讓聚集只實現本身的基本功能,把迭代功能委托給外部類實現,符合類的單一職責設計原則。

2,隱藏聚集的實現細節,符合最小知道原則。為聚集或其子容器提供了一個統一接口,一方面方便客戶端調用;另一方面使得客戶端不必關注迭代器的實現細節。

3,可以為聚集或其子容器實現不同的迭代器,搭配其他設計模式,比如策略模式等,可以很容易的切換。 

4、客戶端可以同時使用多個迭代器遍歷一個聚集。

內部迭代器和外部迭代器

截止到此處,都是分析的外部迭代器模式——客戶端來調用 next 方法,去取得下一個元素。

相反,內部迭代器是由迭代器自己控制游標,在這種情況下,必須告訴迭代器在游標移動的過程中,要做什么事情——必須將操作傳給迭代器,因為內部迭代器的客戶端,無法控制遍歷過程,所欲內部迭代器伸縮性不強,一般不使用。

List 迭代的方向問題

都知道,next 方法是正向遍歷,那么自然可以實現反向遍歷,新加一個取得前一個元素的方法 + 一個判斷游標是否已經走到了首節點的方法即可解決。

JDK也為我們做了實現:ListIterator接口,提供了一個previous方法,JDK中的任何實現了List接口的集合,都可以實現反向迭代。

非線性數據結構的迭代問題

澄清一個問題——迭代器模式是沒有約束元素順序的,即 next (previous)只是取出元素,並不是強制元素取出的先后順序等價於元素的某種排序。通俗的說,不論是線性結構還是非線性的,甚至是包含重復元素的結構,除非有特殊業務需求,都能對其實現迭代器模式。

不可幻想:迭代的順序就等價於集合中元素的某種有意義的排序,兩者沒有必然關系,謹記以避免做出錯誤判斷,除非有自定義的順序約束。

單一職責設計原則和迭代器模式

設計原則:一個類只有一個引起變化的原因。如果有一個類具有兩個改變的原因,那么這會使得將來該類的變化機率上升,而當它真的改變時,你的設計中同時又有兩個方面將會受到影響。

高內聚 > 單一職責原則

內聚:用來度量一個類或者模塊緊密的達到了單一職責的目的(or 責任)。當一個類或者一個模塊被設計為只支持一組相關的功能的時候,就說它具有高內聚的特性,反之就是低內聚的。

高內聚是一個比單一職責更普遍的概念,即遵守了高內聚的類,也同樣具有單一職責。

迭代器模式就遵循了單一職責原則

其實前面的分析已經很全面,迭代器模式,分離了聚集的迭代的責任,有效的契合了單一職責設計原則。

擴展案例:合並咖啡廳的菜單系統

為其合並后的系統,增加咖啡廳的菜單,供應晚餐。下面是咖啡廳的菜單系統實現:

import java.util.HashMap;
import java.util.Map;

/**
 * 原始的咖啡廳菜單實現類
 */
public class CafeMenu {
    /**
     * 菜單使用了hash表存儲,和現有的兩個菜單系統實現不一樣
     */
    private Map<String, MenuItem> menuItems = new HashMap<>();

    public CafeMenu() {
        addItem("Veggie Burger and Air Fries", "Veggie burger on a whole wheat bun, lettuce, tomato, and fries");
    }

    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description);
        menuItems.put(menuItem.getName(), menuItem);
    }

    public Map<String, MenuItem> getItems() {
        return menuItems;
    }
}
 
//////////////////////////////////////////////////
public class MenuItem {
    private String name;
    private String description;public MenuItem(String name,
                    String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }
}

將咖啡廳菜單系統合並到現有的系統:

public interface Menu {
    Iterator createIterator();
}
 
//////////////////////// 咖啡廳菜單系統
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * 合並之后的咖啡廳菜單實現類
 * hash表也實現了JDK的迭代器,不需要RD自己實現
 */
public class CafeMenu implements Menu {
    /**
     * 菜單使用了hash表存儲,和現有的兩個菜單系統實現不一樣
     * 實現不變
     */
    private Map<String, MenuItem> menuItems = new HashMap<>();

    // 實現不變
    public CafeMenu() {
        addItem("Veggie Burger and Air Fries",
                "Veggie burger on a whole wheat bun, lettuce, tomato, and fries");
    }

    // 實現不變
    public void addItem(String name, String description) {
        MenuItem menuItem = new MenuItem(name, description);
        menuItems.put(menuItem.getName(), menuItem);
    }

    /**
     * hash表支持JDK自帶的迭代器 java.util.Iterator;
     */
    @Override
    public Iterator createIterator() {
        // 返回 java.util.Iterator; 只需要取得 hash 表的 value 集合即可
        return menuItems.values().iterator();
    }
}
 
//////////////////////// 客戶端 Waitress
import java.util.Iterator;

public class Waitress {
    private Menu pancakeHouseMenu;
    private Menu dinerMenu;
    // 需要增加 cafeMenu
    private Menu cafeMenu;

    /**
     * 如果,太多的參數,可以使用建造者模式優化構造器
     * 需要增加 cafeMenu 參數
     */
    public Waitress(Menu pancakeHouseMenu, Menu dinerMenu, Menu cafeMenu) {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinerMenu = dinerMenu;
        this.cafeMenu = cafeMenu;
    }

    /**
     * 需要增加 cafeMenu 的迭代器
     */
    public void printMenu() {
        Iterator pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator dinerIterator = dinerMenu.createIterator();
        Iterator cafeIterator = cafeMenu.createIterator();
        printMenu(pancakeIterator);
        printMenu(dinerIterator);
        printMenu(cafeIterator);
    }

    /**
     * 無需修改
     */
    private void printMenu(Iterator iterator) {
        while (iterator.hasNext()) {
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.print(menuItem.getName() + ", ");
            System.out.print(menuItem.getPrice() + " -- ");
            System.out.println(menuItem.getDescription());
        }
    }
}
 
//////////////////////// 測試
public class MenuTestDrive {
    public static void main(String args[]) {
        Menu pancakeHouseMenu = new PancakeHouseMenu();
        Menu dinerMenu = new DinerMenu();
        Menu cafeMenu = new CafeMenu();

        Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu, cafeMenu);
        waitress.printMenu();
    }
}

繼續發現系統的問題——客戶端違反了開閉原則

合並咖啡廳的過程中,發現每次合並新菜單,都要打開客戶端,修改代碼……客戶端實現很丑陋,違反了開閉原則。

雖然我們抽象了菜單,讓其在客戶端解耦,並且為菜單系統分別實現了迭代器,讓迭代責任分離,對客戶端隱藏了具體實現,使用同一的迭代器接口,解耦了迭代動作。但是,仍然將菜單處理分成獨立的對象看待,導致每次擴展,都需要修改客戶端——客戶端需要反復寫:調用printMenue的代碼,代碼冗余嚴重,而且每次都要給構造器增加新參數。

需要一種更好的辦法——集中管理菜單,使其使用一個迭代器即可應付菜單的擴展

解決方案:抽象客戶端各個獨立的菜單系統,只需保留一個迭代器

使用現成的 ArrayList 類實現:

import java.util.Iterator;
import java.util.List;

public class Waitress1 {
    /**
     * 把各個菜單系統集中到一個list,充分利用list的迭代器
     * 只需要一個類就搞定,不再每次都add一個菜單類了
     */
    private List<Menu> menus;

    public Waitress1(List<Menu> menus) {
        this.menus = menus;
    }

    public void printMenu() {
        // 取得list的迭代器,直接使用一個迭代器,就能遍歷所有菜單,不需要在修改
        Iterator menuIterator = menus.iterator();
        while (menuIterator.hasNext()) {
            Menu menu = (Menu) menuIterator.next();
            printMenu(menu.createIterator());
        }
    }

    // 代碼不需要變
    void printMenu(Iterator iterator) {
        while (iterator.hasNext()) {
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.print(menuItem.getName() + ", ");
            System.out.print(menuItem.getPrice() + " -- ");
            System.out.println(menuItem.getDescription());
        }
    }
}

基於迭代器模式實現的菜單系統無法實現樹狀菜單(無法擴展子菜單)

現在希望能夠加上一份餐后甜點“子菜單”作為晚餐的飯后補充。如果我們能讓甜點菜單變成餐廳菜單集合的一個子元素,就可以完美的解決。但是根據現在的實現,根本做不到。因為飯后甜點子菜單的實現基於數組——不變的,類型不同,無法擴展。生產環境中,這樣的系統非常復雜,更加困難。

解決方案——樹

1、需要某種樹形結構,可以容納菜單、子菜單和菜單項。

2、需要確定能夠在每個菜單的各個項目之間游走,而且至少要像現在用迭代器一樣方便。

3、需要能夠更有彈性地在菜單項之間游走。比方說:可能只需要遍歷甜點菜單,或者可以遍歷餐廳的整個菜單。

此時,需要一種新的設計模式來解決這個案例的難題——組合模式,參看:優雅的處理樹狀結構——組合模式總結

歡迎關注

dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM