zoukankan      html  css  js  c++  java
  • 遍历“容器”的优雅方法——总结迭代器模式

    前言

    本文主要是读书笔记的整理,自己总结的倒不多,做个记录

    聚集(集合)的概念

    如果能把多个普通类的对象聚在一起形成一个总体,这个总体就被称之为聚集(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电子书,资料,帮忙内推,欢迎拍砖!

  • 相关阅读:
    Java对象序列化文件追加对象的问题,以及Java的读取多个对象的问题解决方法。
    解决chrome在docky上的图标模糊或不能锁定的问题
    获取表单中的输入内容、单选按钮、复选框的输入内容
    用idea写servlet文件
    get方法和post方法
    解决Only a type can be imported. com.mysql.jdbc.Connection resolves to a package的报错问题
    idea中如何配置tomcat
    JDBC中的PreparedStatement
    JDBC中的ResultSet
    JDBCl链接中Statement
  • 原文地址:https://www.cnblogs.com/kubixuesheng/p/5183739.html
Copyright © 2011-2022 走看看