遍历“容器”的优雅方法——总结迭代器模式

前言

本文主要是读书笔记的整理,本身总结的倒很少,作个记录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());
        }
    }
}

基于迭代器模式实现的菜单系统没法实现树状菜单(没法扩展子菜单)

如今但愿可以加上一份餐后甜点“子菜单”做为晚餐的饭后补充。若是咱们能让甜点菜单变成餐厅菜单集合的一个子元素,就能够完美的解决。可是根据如今的实现,根本作不到。由于饭后甜点子菜单的实现基于数组——不变的,类型不一样,没法扩展。生产环境中,这样的系统很是复杂,更加困难。

解决方案——树

一、须要某种树形结构,能够容纳菜单、子菜单和菜单项。

二、须要肯定可以在每一个菜单的各个项目之间游走,并且至少要像如今用迭代器同样方便。

三、须要可以更有弹性地在菜单项之间游走。比方说:可能只须要遍历甜点菜单,或者能够遍历餐厅的整个菜单。

此时,须要一种新的设计模式来解决这个案例的难题——组合模式,参看:优雅的处理树状结构——组合模式总结

相关文章
相关标签/搜索