設計模式學習筆記十一------迭代器模式

目錄

本文的結構如下:

  • 引言
  • 什麼是迭代器模式
  • 模式的結構
  • 典型代碼
  • 代碼示例
  • 優點和缺點
  • 適用環境
  • 模式應用

一、引言

在平時生活中,可能有這樣的場景,一天的高強度敲代碼特別疲累,下班後又在十字路口堵了大半天,好不容易回到家中,啥也不想幹,就往沙發上一躺,拿起遙控器,打開電視,選了一個愛看的頻道,哇,全是美女,好吧,可惜太累了,居然睡着了。

這裏的電視就是一個存放頻道的容器,而遙控器則方便我們去訪問這個電視裏的頻道,並且壓根不知道電視到底是怎麼存放頻道的,也不需要知道。

在軟件開發中,也存在像電視這樣的類,它用來存儲多個成員對象,這些類稱爲聚合類(Aggregate Classes),對應的對象稱爲聚合對象。爲了更加方便地操作這些聚合對象,同時可以很靈活地爲聚合對象增加不同的遍歷方法,可以爲這些類提供一個遙控器樣的角色,可以訪問一個聚合對象中的元素但又不需要暴露它的內部結構,稱爲迭代器。

這種設計其實就是迭代器模式。

二、什麼是迭代器模式

聚合對象用來存儲一系列數據。其主要有兩個職責:一是存儲數據;二是遍歷數據。從依賴性來看,前者是聚合對象的基本職責;而後者既是可變化的,又是可分離的。這時,將遍歷數據的行爲從聚合對象中分離出來,封裝在一個被稱之爲“迭代器”的對象中,由迭代器來提供遍歷聚合對象內部數據的行爲,這將簡化聚合對象的設計,更符合“單一職責原則”的要求。

迭代器模式定義如下:

迭代器模式(Iterator Pattern):提供一種方法來訪問聚合對象,而不用暴露這個對象的內部表示,其別名爲遊標(Cursor)。迭代器模式是一種對象行爲型模式。

迭代器模式將爲聚合對象提供一個遙控器,通過引入迭代器,客戶端無須瞭解聚合對象的內部結構即可實現對聚合對象中成員的遍歷,還可以根據需要很方便地增加新的遍歷方式。

三、模式的結構

迭代器模式的UML類圖如下:

在迭代器模式中包含的角色有:

Aggregate(抽象聚合類):它用於存儲和管理元素對象,聲明一個createIterator()方法用於創建一個迭代器對象,充當抽象迭代器工廠角色。

ConcreteAggregate(具體聚合類):它實現了在抽象聚合類中聲明的createIterator()方法,該方法返回一個與該具體聚合類對應的具體迭代器ConcreteIterator實例。

Iterator(抽象迭代器):它定義了訪問和遍歷元素的接口,聲明瞭用於遍歷數據元素的方法,例如:用於判斷是否還有下一個元素的hasNext()方法,用於獲取獲取下一個元素的next()方法,在具體迭代器中將實現這些方法。

ConcreteIterator(具體迭代器):它依賴具體的聚合對象,實現了抽象迭代器接口,完成對聚合對象的遍歷,同時在具體迭代器中通過遊標來記錄在聚合對象中所處的當前位置,在具體實現時,遊標通常是一個表示位置的非負整數。

在迭代器模式中,提供了一個外部的迭代器來對聚合對象進行訪問和遍歷,迭代器定義了一個訪問該聚合元素的接口,並且可以跟蹤當前遍歷的元素,瞭解哪些元素已經遍歷過而哪些沒有。迭代器的引入,將使得對一個複雜聚合對象的操作變得簡單。

四、典型代碼

抽象聚合類的典型代碼如下(這裏是抽象類,也可以是接口):

public abstract class Aggregate<T> {
    abstract Iterator<T> createIterator();
}

具體聚合類的典型代碼如下:

public class ConcreteAggregate<T> extends Aggregate<T> {

    public ConcreteAggregate(List<T> objects) {
        super(objects);
    }

    Iterator<T> createIterator() {
        return new ConcreteIterator<T>(this);
    }
}

抽象迭代器的典型代碼如下:

public interface Iterator<T> {
    boolean hasNext();//判斷是否存在下一個元素
    T next();//返回下個元素
}

具體迭代器的典型代碼如下:

public class ConcreteIterator<T> implements Iterator<T> {

    private ConcreteAggregate<T> concreteAggregate;//維持一個對具體聚合對象的引用,以便於訪問存儲在聚合對象中的數據
    private int cursor; //定義一個遊標,用於記錄當前訪問位置

    public ConcreteIterator(ConcreteAggregate<T> concreteAggregate){
        this.concreteAggregate = concreteAggregate;
    }

    public boolean hasNext() {
        return cursor != concreteAggregate.getObjects().size();
    }

    public T next() {
        //toDo
        return null;
    }
}

五、代碼示例

假設某個公司需要開發一個設備管理系統,該系統需要對用戶數據,設備信息進行遍歷,要實現這個功能,怎麼做呢?

5.1、繼承複用

先看下最常規的做法。

爲了複用遍歷的功能,一般會建立一個抽象的數據集合類AbstractObjectList,裏面實現一些常用的方法用於訪問,然後存放用戶數據和設備數據的類都從AbstractObjectList繼承,則可以複用這些方法。

如上圖所以,也許還有更多的方法,這裏只列舉幾個。

但是這樣設計後,會發現有幾個缺點:

  1. addObject()、removeObject()等方法用於管理數據,而next()、isLast()、previous()、isFirst()等方法用於遍歷數據。這將導致聚合類的職責過重,它既負責存儲和管理數據,又負責遍歷數據,違反了“單一職責原則”,由於聚合類非常龐大,實現代碼過長,還將給測試和維護增加難度。
  2. 如果將抽象聚合類聲明爲一個接口,則在這個接口中充斥着大量方法,不利於子類實現,違反了“接口隔離原則”。
  3. 如果將所有的遍歷操作都交給子類來實現,將導致子類代碼龐大,而且必須暴露AbstractObjectList的內部存儲細節,向子類公開自己的私有屬性,否則子類無法實施對數據的遍歷,這將破壞AbstractObjectList類的封裝性。

5.2、迭代器模式

迭代器模式則可以規避繼承的缺點,它將聚合類中負責遍歷數據的方法提取出來,封裝到專門的類中,實現數據存儲和數據遍歷分離,無須暴露聚合類的內部屬性即可對其進行操作。

抽象聚合類:

public abstract class AbstractObjectList<T> {
    private List<T> objects;

    public AbstractObjectList(List<T> objects){
        this.objects = objects;
    }

    public void addObject(T e){
        this.objects.add(e);
    }

    public void removeObject(T e){
        this.objects.remove(e);
    }

    public List<T> getObjects(){
        return objects;
    }

    abstract AbstractIterator<T> createIterator();
}

具體聚合類:

public class UserList extends AbstractObjectList<User> {
    public UserList(List<User> users) {
        super(users);
    }

    AbstractIterator<User> createIterator() {
        return new UserIterator(this);
    }
}

抽象迭代器:

public interface AbstractIterator<T> {
    public void next(); //移至下一個元素
    public boolean isLast(); //判斷是否爲最後一個元素
    public void previous(); //移至上一個元素
    public boolean isFirst(); //判斷是否爲第一個元素
    public T getNextItem(); //獲取下一個元素
    public T getPreviousItem(); //獲取上一個元素
}

用戶數據迭代類:

public class UserIterator implements AbstractIterator<User> {
    private UserList userList;
    private List<User> users;
    private int cursor1; //定義一個遊標,用於記錄正向遍歷的位置
    private int cursor2; //定義一個遊標,用於記錄逆向遍歷的位置

    public UserIterator(UserList userList) {
        this.userList = userList;
        this.users = userList.getObjects();//獲取集合對象
        cursor1 = 0; //設置正向遍歷遊標的初始值
        cursor2 = users.size() -1; //設置逆向遍歷遊標的初始值
    }

    public void next() {
        if(cursor1 < users.size()) {
            cursor1++;
        }
    }

    public boolean isLast() {
        return (cursor1 == users.size());
    }

    public void previous() {
        if (cursor2 > -1) {
            cursor2--;
        }
    }

    public boolean isFirst() {
        return (cursor2 == -1);
    }

    public User getNextItem() {
        return users.get(cursor1);
    }

    public User getPreviousItem() {
        return users.get(cursor2);
    }
}

測試:

public class Client {
    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();
        users.add(new User("孫悟空", "男娃"));
        users.add(new User("貂蟬", "女娃"));
        users.add(new User("關二爺", "男娃"));
        users.add(new User("紫霞仙子", "女娃"));
        users.add(new User("勒布朗", "男娃"));
        users.add(new User("胡歌", "男娃"));

        AbstractObjectList<User> list;//創建聚合對象
        AbstractIterator<User> iterator;//創建迭代器對象

        list = new UserList(users);
        iterator = list.createIterator();

        System.out.println("正向遍歷:");
        while (!iterator.isLast()){
            System.out.println(iterator.getNextItem() + ",");
            iterator.next();
        }

        System.out.println();
        System.out.println("-----------------------------");
        System.out.println("逆向遍歷:");
        while (!iterator.isFirst()){
            System.out.println(iterator.getPreviousItem() + ",");
            iterator.previous();
        }
    }
}

如果需要增加一個新的具體聚合類,如設備數據集合類,並且需要爲設備數據集合類提供不同於用戶數據集合類的正向遍歷和逆向遍歷操作,只需增加一個新的聚合子類和一個新的具體迭代器類即可,原有類庫代碼無須修改,符合“開閉原則”;
如果需要爲用戶聚合類更換一個迭代器,只需要增加一個新的具體迭代器類作爲抽象迭代器類的子類,重新實現遍歷方法,原有迭代器代碼無須修改,也符合“開閉原則”;
但是如果要在迭代器中增加新的方法,則需要修改抽象迭代器源代碼,這將違背“開閉原則”。

5.3、使用內部類實現迭代器

在迭代器模式結構圖中,具體迭代器類和具體聚合類之間存在雙重關係,其中一個關係爲關聯關係,在具體迭代器中需要維持一個對具體聚合對象的引用,該關聯關係的目的是訪問存儲在聚合對象中的數據,以便迭代器能夠對這些數據進行遍歷操作。

除了使用關聯關係外,爲了能夠讓迭代器可以訪問到聚合對象中的數據,我們還可以將迭代器類設計爲聚合類的內部類。

public class UserList extends AbstractObjectList<User> {
    public UserList(List<User> users) {
        super(users);
    }

    AbstractIterator<User> createIterator() {
        return new UserIterator(this);
    }

    private class Itr implements AbstractIterator<User> {
        private int cursor1;
        private int cursor2;

        public Itr(){
            cursor1 = 0;
            cursor2 = getObjects().size() -1;
        }

        public void next() {
            if(cursor1 < getObjects().size()) {
                cursor1++;
            }
        }

        public boolean isLast() {
            return (cursor1 == getObjects().size());
        }

        public void previous() {
            if(cursor2 > -1) {
                cursor2--;
            }
        }

        public boolean isFirst() {
            return (cursor2 == -1);
        }

        public User getNextItem() {
            return getObjects().get(cursor1);
        }

        public User getPreviousItem() {
            return getObjects().get(cursor2);
        }
    }
}

用不用內部類實現,對客戶端來說都是一樣的,客戶端無須關心具體迭代器對象的創建細節,只需通過調用工廠方法createIterator()即可得到一個可用的迭代器對象。

說到底,其實迭代器模式就是讓聚合類的管理數據的責任和遍歷的責任分離。

六、優點和缺點

6.1、優點

迭代器模式的主要優點如下:

  • 它支持以不同的方式遍歷一個聚合對象,在同一個聚合對象上可以定義多種遍歷方式(比如JDK ArrayList中,有Iterator和ListIterator)。在迭代器模式中只需要用一個不同的迭代器來替換原有迭代器即可改變遍歷算法,我們也可以自己定義迭代器的子類以支持新的遍歷方式。
  • 迭代器簡化了聚合類。由於引入了迭代器,在原有的聚合對象中不需要再自行提供數據遍歷等方法,這樣可以簡化聚合類的設計,符合“類的職責單一原則”。
  • 在迭代器模式中,由於引入了抽象層,增加新的聚合類和迭代器類都很方便,無須修改原有代碼,滿足“開閉原則”的要求。

6.2、缺點

迭代器模式的主要缺點如下:

  • 由於迭代器模式將存儲數據和遍歷數據的職責分離,增加新的聚合類需要對應增加新的迭代器類,類的個數成對增加,這在一定程度上增加了系統的複雜性。
  • 抽象迭代器的設計難度較大,需要充分考慮到系統將來的擴展,例如JDK內置迭代器Iterator就無法實現逆向遍歷,如果需要實現逆向遍歷,只能通過其子類ListIterator等來實現,而ListIterator迭代器無法用於操作Set類型的聚合對象。在自定義迭代器時,創建一個考慮全面的抽象迭代器並不是件很容易的事情。

七、適用場景

在以下情況下可以考慮使用迭代器模式:

  • 訪問一個聚合對象的內容而無須暴露它的內部表示。將聚合對象的訪問與內部數據的存儲分離,使得訪問聚合對象時無須瞭解其內部實現細節。
  • 需要爲一個聚合對象提供多種遍歷方式。
  • 爲遍歷不同的聚合結構提供一個統一的接口,在該接口的實現類中爲不同的聚合結構提供不同的遍歷方式,而客戶端可以一致性地操作該接口。

迭代器模式是一種使用頻率非常高的設計模式,通過引入迭代器可以將數據的遍歷功能從聚合對象中分離出來,聚合對象只負責存儲數據,而遍歷數據 由迭代器來完成。由於很多編程語言的類庫都已經實現了迭代器模式,因此在實際開發中,我們只需要直接使用Java、C#等語言已定義好的迭代器即可,迭代器已經成爲我們操作聚合對象的基本工具之一。

八、模式應用

JDK內置的迭代器就是一個很好的例子。

在Java集合框架中,常用的List和Set等聚合類都繼承(或實現)了java.util.Collection接口,在Collection接口中聲明瞭如下方法(部分):

裏面有一個iterator方法,返回一個迭代器,用於數據的遍歷。

Iterator接口有幾個方法:

JDK中的模式很複雜,從idea中掏出List的部分類圖看看:

Collection接口和Iterator接口充當了迭代器模式的抽象層,分別對應於抽象聚合類和抽象迭代器,而Collection接口的子類充當了具體聚合類,Iterator的子類則充當了具體迭代器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章