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

前言

本文主要是讀書筆記的整理,本身總結的倒很少,作個記錄html

彙集(集合)的概念

若是能把多個普通類的對象聚在一塊兒造成一個整體,這個整體就被稱之爲彙集(Aggregate),舉例子:java

一、在任何編程語言中:數組都是最基本的彙集,在Java中,數組也是其餘的 JAVA 彙集對象的設計基礎。算法

二、在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 代碼:app

/**
 * 餐廳的菜單都是午飯項目,煎餅的菜單,都是早餐項目,可是它們都屬於菜單,即:
 * 都有菜品名稱,描述,是不是素的,價格等
 * 故設計這樣一個類做爲菜單項目類
 */
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都有很大重複代碼,

沒有抽象共享

那麼能夠解決麼?

解決方法

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

二、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,因此咱們就不用本身寫,只需實現數組的迭代器便可。

一、記住:JDK 不支持爲數組生成迭代器

二、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 異常

參考

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

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

Fail Fast 問題

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

使用迭代器模式的優勢

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

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

  • 按順序訪問

優勢總結:

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

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

3,能夠爲彙集或其子容器實現不一樣的迭代器,搭配其餘設計模式,好比策略模式等,能夠很容易的切換。 

四、客戶端能夠同時使用多個迭代器遍歷一個彙集。

內部迭代器和外部迭代器

截止到此處,都是分析的外部迭代器模式——客戶端來調用 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());
        }
    }
}

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

如今但願可以加上一份餐後甜點「子菜單」做爲晚餐的飯後補充。若是咱們能讓甜點菜單變成餐廳菜單集合的一個子元素,就能夠完美的解決。可是根據如今的實現,根本作不到。由於飯後甜點子菜單的實現基於數組——不變的,類型不一樣,沒法擴展。生產環境中,這樣的系統很是複雜,更加困難。

解決方案——樹

一、須要某種樹形結構,能夠容納菜單、子菜單和菜單項。

二、須要肯定可以在每一個菜單的各個項目之間遊走,並且至少要像如今用迭代器同樣方便。

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

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

相關文章
相關標籤/搜索