GoF23:Iterator & Composite(迭代器&组合)

GoF23

1、case:菜单

有两份菜单

  • 菜单项结构相同;
  • 菜单项存储的实现不同:使用不同的“集合”。

image-20220209231258520

1.1、MenuItem

  • 属性:名称、价格
  • 构造器初始化
  • getter
public class MenuItem {
    private String name;
    private double price;

    public MenuItem(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }
}

1.2、现有的实现

1.2.1、Menu

  • MenuItem 的存储方式
    • 煎饼屋:ArrayList,易于扩展菜单;
    • 餐厅:数组,允许控制长度。
  • 构造器中初始化集合,添加 Item;
  • addItem() 和 getMenuItems() 方法。

煎饼屋:ArrayList

public class PancakeMenu {
    private ArrayList<MenuItem> menuItems;

    public PancakeMenu() {
        menuItems = new ArrayList<>();

        addItem("K&B's Pancake Breakfast", 2.99);
        addItem("Regular Pancake Breakfast", 2.99);
        addItem("Blueberry Pancake", 3.49);
        addItem("Waffles", 3.59);
    }

    private void addItem(String name, double price) {
        MenuItem item = new MenuItem(name, price);

        menuItems.add(item);
    }

    public ArrayList<MenuItem> getMenuItems() {
        return menuItems;
    }
}

餐厅:数组

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

    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];

        addItem("Vegetarian BLT", 2.99);
        addItem("BLT", 2.99);
        addItem("Soup of the day", 3.29);
        addItem("Hot dog", 3.05);
    }

    private void addItem(String name, double price) {
        MenuItem item = new MenuItem(name, price);

        if (position >= MAX_ITEMS) {
            System.out.println("Menu is full!");
        } else {
            menuItems[position++] = item;
        }
    }

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

1.2.2、Waiter

服务员:根据客户需求,打印相应的菜单

  • 成员变量:两份菜单,构造器注入;
  • 方法:分别遍历集合,打印相应的菜单。
public class Waiter {
    private PancakeMenu pancakeMenu;
    private DinerMenu dinerMenu;

    public Waiter(PancakeMenu pancakeMenu, DinerMenu dinerMenu) {
        this.pancakeMenu = pancakeMenu;
        this.dinerMenu = dinerMenu;
    }

    public void printPancakeMenu() {
        System.out.println("====== Pancake Menu ======");

        ArrayList<MenuItem> menuItems = pancakeMenu.getMenuItems();

        for (int i = 0; i < menuItems.size(); i++) {
            MenuItem menuItem = menuItems.get(i);

            System.out.print(menuItem.getPrice() + "\t");
            System.out.println(menuItem.getName());
        }
    }

    public void printDinerMenu() {
        System.out.println("====== Diner Menu ======");

        MenuItem[] menuItems = dinerMenu.getMenuItems();

        for (int i = 0; i < dinerMenu.getSize(); i++) {
            MenuItem menuItem = menuItems[i];
            
            System.out.print(menuItem.getPrice() + "\t");
            System.out.println(menuItem.getName());
        }
    }
}

1.2.3、测试

@Test
public void test() {
    Waiter waiter = new Waiter(new PancakeMenu(), new DinerMenu());
    waiter.printPancakeMenu();
    waiter.printDinerMenu();
}

image-20220210162203036

1.2.4、分析

维护性、扩展性差

Menu

  1. 违反单一职责原则:既负责存储 Item,又负责遍历工作;
  2. 针对实现编程:没有设计接口,而是设计两个单独的类。

Waiter

  1. 代码重复:每个 Menu 都需使用一次循环,而区别仅在于循环上界和获取集合项的方式;
  2. 针对实现编程:针对具体的 Menu 编程,而不是针对接口编程;
  3. 违反封装:本类需要知道每个 Menu 的具体实现,以采取相应的遍历方式;
  4. 违反开闭原则:若 MenuItem 存储方式改变(如 Hashtable),需要修改类的代码。

image-20220210001056263

2、自定义迭代器

  • Java API 有内置的迭代器:java.util.Iterator;
  • 先实现自定义迭代器,再使用 Java 的迭代器API。

2.1、迭代器:封装遍历

迭代器接口:封装 ”遍历集合的过程“。

  • hasNext():判断是否有更多元素;
  • next():返回下一个元素。
public interface MenuIterator {
    boolean hasNext();
    Object next();
}

实现

实现 MenuIterator 接口。

  • 成员变量
    • menuItems:item 集合,构造器注入;
    • position:记录当前遍历的位置。
  • 实现方法
    • hasNext():position 不超过集合的容量,且集合项不为 null;
    • next():返回下一个元素,之后 position 自增。
// ArrayList
public class PancakeMenuIterator implements MenuIterator {
    private ArrayList<MenuItem> menuItems;
    private int position;

    public PancakeMenuIterator(ArrayList<MenuItem> menuItems) {
        this.menuItems = menuItems;
    }

    @Override
    public boolean hasNext() {
        return position < menuItems.size() && menuItems.get(position) != null;
    }

    @Override
    public Object next() {
        return menuItems.get(position++);
    }
}
// 数组
public class DinerMenuIterator implements MenuIterator {
    private MenuItem[] menuItems;
    private int position;

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

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

    @Override
    public Object next() {
        return menuItems[position++];
    }
}

2.2、Menu

Menu 的遍历工作交给迭代器完成,而 Menu 需提供获取迭代器的方法。

将 Menu 定义成接口,提供 createIterator() 方法:

  • 用途:创建迭代器;
  • 使用了 工厂方法模式:非参数化工厂方法、只生产一种对象(即迭代器)。
public interface Menu {
    MenuIterator createIterator();
}

改进

  1. 实现 Menu 接口,实现 createIterator() 方法;
  2. 去掉 getMenuItems() 方法。

实现

  • PancakeMenu

    image-20220210161140390

  • DinerMenu

    image-20220210161217454

2.3、Waiter

  1. 成员变量、构造器参数:声明为接口类型;
  2. 定义方法 printMenu():通过迭代器遍历集合;
  3. 原有方法
    • 获取菜单集合项 → 获取菜单迭代器;
    • 遍历循环体 → 委托 printMenu() 遍历。

image-20220210163826511

2.4、测试

结果完全一样。

@Test
public void test() {
    Waiter waiter = new Waiter(new PancakeMenu(), new DinerMenu());
    waiter.printPancakeMenu();
    waiter.printDinerMenu();
}

image-20220210162203036

2.5、分析

  1. 遵循单一职责原则:Menu 负责管理集合,而将遍历集合的职责交给了迭代器。
    • 之前:通过 Menu 获取集合,由 Menu 的客户进行遍历;
    • 现在:通过 Menu 获取迭代器,由 迭代器 进行遍历;
  2. 针对接口编程:设计接口,便于客户解耦。

Waiter

  1. 减少代码重复:只定义一个方法用于遍历集合,将请求委托给该方法;
  2. 针对接口编程:成员变量、构造器参数,声明为接口类型;
  3. 封装:Waiter 作为 Menu 的客户,不再需要关注 Menu 集合的实现细节。

3、Java API:迭代器

  • 通过以上实例,学会自定义迭代器;
  • 接下来,使用 java.util.Iterator 改进本方案。

3.1、迭代器&集合

Iterator

迭代器:java.util.Iterator

image-20220210170712077

相比自定义迭代器,多了两个方法:

  • remove():删除最后一个返回的元素;
  • forEachRemaining():JDK 1.8 新增。对剩余元素执行给定动作。

Collection

集合:java.util.Collection

  • 也称为 聚合(aggregate),指一群对象;
  • 可以是各种数据结构:列表、数组、散列表等。

image-20220210181403781

提供了抽象方法 iterator()

  • 功能:获取当前集合对象的迭代器;

  • Collection 是一个庞大的集合体系,其子类都实现了 iterator()

image-20220210180814322

3.2、改进

引入 java.util.Iterator 作为迭代器。

3.2.1、迭代器

  1. ArrayList

    • 具有 iterator() 方法,无需使用自定义迭代器;

    • 去除 PancakeMenuIterator 类。

  2. 数组

    • 不具有 iterator() 方法,仍需使用自定义迭代器;

    • 修改 DinerMenuIterator 类,实现 java.util.Iterator 接口;

    • 实现 hasNext() 和 next(),代码无改动;

    • 重写 remove() 方法

      @Override
      public void remove() {
          if (position <= 0) {
              throw new IllegalStateException("you haven't done at lease one next()");
          }
          if (menuItems[position - 1] != null) {
              // 从position-1开始,从后往前覆盖
              for (int i = position - 1; i < menuItems.length - 1; i++) {
                  menuItems[i] = menuItems[i + 1];
              }
              // 最后一个值为null
              menuItems[menuItems.length - 1] = null;
          }
      }
      

3.2.2、Menu

接口及实现类:createIterator() 返回值修改为 Iterator。

import java.util.Iterator;

public interface Menu {
    Iterator createIterator();
}
  1. PancakeMenu:createIterator() 返回集合的 iterator()

    image-20220210183655326

  2. DinerMenu:无需修改代码

    image-20220210183720808

3.2.3、类图

image-20220210191555288

3.3、添加新菜单

3.3.1、CafeMenu

添加一个 CafeMenu,测试 HashTable 集合。

  • HashTable:存储 K-V 键值对,构造器注入并赋值;
  • addItem():调用 HashTable 的 put 方法存储;
  • createIterator():由于集合存储的是实体对,而我们实际要遍历的只是 value,因此调用 values 的 Iterator
public class CafeMenu implements Menu {
    private Hashtable<String, MenuItem> menuItems;

    public CafeMenu() {
        menuItems = new Hashtable<>();

        addItem("House blend", 0.89);
        addItem("Decaf", 1.05);
        addItem("Dark Roast", 0.99);
    }

    private void addItem(String name, double price) {
        MenuItem item = new MenuItem(name, price);

        menuItems.put(item.getName(), item);
    }

    @Override
    public Iterator createIterator() {
        return menuItems.values().iterator();
    }
}

3.3.2、Waiter

添加一个成员变量,以及对应的构造器参数、打印方法。

private Menu cafeMenu;

public Waiter(Menu pancakeMenu, Menu dinerMenu, Menu cafeMenu) {
    this.pancakeMenu = pancakeMenu;
    this.dinerMenu = dinerMenu;
    this.cafeMenu = cafeMenu;
}

public void printCafeMenu() {
    System.out.println("====== Cafe Menu ======");

    Iterator iterator = cafeMenu.createIterator();
    printMenu(iterator);
}

3.3.3、测试

@Test
public void test() {
    Waiter waiter = new Waiter(new PancakeMenu(), new DinerMenu(), new CafeMenu());
    waiter.printCafeMenu();
}

image-20220210213638410

4、迭代器模式

4.1、定义

迭代器模式:提供一种方法,顺序访问一个聚合对象的各个元素,而不暴露内部表示

  • 聚合对象将遍历元素的职责交给迭代器,其本身可以专注于管理对象集合;

  • 通常会使用 工厂方法模式 来创建迭代器;

  • OOP原则:单一职责原则;

  • 类图

    image-20220210205219152

  • 内部 & 外部迭代器

    • 外部迭代器:客户调用 next() 取得下一个元素;
    • 内部迭代器:迭代器自行在元素之间游走。
public interface Menu {
    MenuIterator createIterator();
}

4.2、相关机制

枚举器

在集合框架中,迭代器(JDK1.2)取代了枚举器(JDK 1.0)的地位,有以下区别

  1. 方法名称改善;
  2. 迭代器支持删除操作,在 JDK1.8 后还引入 forEachRemaining() 方法。

增强for循环

JDK1.5 提供的 for/in 语句,即增强 for 循环。

  • 支持在集合或数组中遍历,且不需要显式创建迭代器。

  • 语法

    • collection:被遍历的集合或数组;
    • obj:每次遍历的一个元素。
    for (Object obj: collection){
        ...
    }
    

5、组合模式

定义

组合模式将对象组合成树形结构,来表现 “整体/部分” 层次结构。

  • 建立树形结构,包含个别对象和对象组合;
  • 让客户能把相同的操作,应用在个别对象和组合上;
  • OOP原则——里氏替换:继承必须使父类的性质在子类中成立。

类图

做法:定义一个抽象组件类,作为个别对象和对象组合的超类。

  • Client:针对接口编程,通过 Component 操作组合中的对象;
  • Component:组件
    • 包含叶节点(Leaf)和组合(Composite);
    • 提供一些方法及默认实现:如增加、删除、获取子节点,其它操作元素的行为。
  • Composite:组合
    • 包含子节点的组件;
    • 重写了所有超类方法,定义组件的行为。
  • Leaf:叶节点
    • 没有子节点的组件;
    • 重写了操作元素行为的方法,定义组合内元素的行为;

image-20220210220931084

6、case:子菜单

需求:在 DinerMenu 中加入子菜单:甜品菜单。

6.1、设计

树状结构

将 Menu 体系设计成树状结构

  • 组件:菜单,如DinerMenu;
  • 叶节点:菜单项,即MenuItem;
  • 组合:子菜单,如DessertMenu。

image-20220211005152976

类图

  • Waiter:针对接口编程,通过 MenuComponent 操作组合中的对象;
  • MenuComponent:组件
    • 包含叶节点(MenuItem)和组合(Menu);
    • 提供一些方法及默认实现:如增加、删除、获取子节点,其它操作元素的行为。
  • Menu:组合,包含子节点;
  • Leaf:叶节点,没有子节点;

image-20220210234647005

6.2、实现

组件:MenuComponent

  • 提供一些方法,默认实现为抛出 UnsupportedOperationException 异常
  • 牺牲了单一职责原则,换取透明性
    • 组件接口同时包含管理集合、操作集合的职责(内部迭代器);
    • 一个元素是组合还是叶节点,对客户是透明的,可以同等处理。
public abstract class MenuComponent {
    public void print() {
        throw new UnsupportedOperationException();
    }

    public void add(MenuComponent component) {
        throw new UnsupportedOperationException();
    }

    public void remove(MenuComponent component) {
        throw new UnsupportedOperationException();
    }

    public MenuComponent getChild(int i) {
        throw new UnsupportedOperationException();
    }

    public String getName() {
        throw new UnsupportedOperationException();
    }

    public double getPrice() {
        throw new UnsupportedOperationException();
    }
}

组合:Menu

  • 相比之前的 Menu,不再通过构造器为集合赋初始值;
  • 而是由 Menu 的客户来赋值,如 业务中的厨师 为菜单添加菜谱。
  • name:菜单名称
    • 在之前,我们为每个菜单创建一个类,而类名就是菜单名称;
    • 由于引入了组合模式,Menu 组合类,代表所有的菜单;
  • componentList:组件集合
    • 用于记录所有的组件,包括组合(子菜单)和叶节点(菜单项);
    • 菜单可以有任意数量的 MenuComponent 子节点;
  • print:打印菜单
    • 使用内部迭代器,遍历 componentList 集合,递归调用每个组件的 print() 方法;
    • 也可以使用增强 for 循环。
public class Menu extends MenuComponent {
    private String name;
    private ArrayList<MenuComponent> componentList;

    public Menu(String name) {
        this.name = name;
        componentList = new ArrayList<>();
    }

    @Override
    public void print() {
        System.out.println("======= " + name + " =======");
        Iterator<MenuComponent> iterator = componentList.iterator();
        while(iterator.hasNext()){
            MenuComponent component = iterator.next();
            component.print();
        }
        // for (MenuComponent component : componentList) {
        //     System.out.print("--");
        //     component.print();
        // }
    }

    @Override
    public void add(MenuComponent component) {
        componentList.add(component);
    }

    @Override
    public void remove(MenuComponent component) {
        componentList.remove(component);
    }

    @Override
    public MenuComponent getChild(int i) {
        return componentList.get(i);
    }

    @Override
    public String getName() {
        return name;
    }
}

叶节点:MenuItem

在原来 MenuItem 的基础上

  • 继承 MenuComponent 超类;
  • 重写 print() 方法。
public class MenuItem extends MenuComponent {
    private String name;
    private double price;

    public MenuItem(String name, double price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public double getPrice() {
        return price;
    }

    @Override
    public void print() {
        System.out.println("\t" + price + "\t" + name);
    }
}

客户:Waiter

  • 成员变量:针对接口编程,构造器注入;
  • printMenu():委托 component 的 print()。
public class CompositeWaiter {
    private MenuComponent component;

    public CompositeWaiter(MenuComponent component) {
        this.component = component;
    }

    public void printMenu() {
        component.print();
    }
}

6.3、测试

  1. 创建所有菜单;
  2. 创建顶级菜单,添加所有菜单;
  3. 添加菜单项;
  4. 创建Menu的客户:Waiter,打印菜单。
@Test
public void test() {
    // 创建所有菜单
    MenuComponent pancakeMenu = new Menu("Pancake Menu");
    MenuComponent dinerMenu = new Menu("Diner Menu");
    MenuComponent cafeMenu = new Menu("Cafe Menu");
    MenuComponent dessertMenu = new Menu("Dessert Menu");
    // 创建顶级菜单,添加所有菜单
    MenuComponent allMenu = new Menu("All Menu");
    allMenu.add(pancakeMenu);
    allMenu.add(dinerMenu);
    allMenu.add(cafeMenu);

    // 添加菜单项
    // PancakeMenu
    pancakeMenu.add(new MenuItem("K&B's Pancake Breakfast", 2.99));
    pancakeMenu.add(new MenuItem("Regular Pancake Breakfast", 2.99));
    pancakeMenu.add(new MenuItem("Blueberry Pancake", 3.49));
    pancakeMenu.add(new MenuItem("Waffles", 3.59));
    // DinerMenu
    dinerMenu.add(new MenuItem("Vegetarian BLT", 2.99));
    dinerMenu.add(new MenuItem("BLT", 2.99));
    dinerMenu.add(new MenuItem("Soup of the day", 3.29));
    dinerMenu.adds(new MenuItem("Hot dog", 3.05));
    dinerMenu.add(dessertMenu);
    // CafeMenu
    cafeMenu.add(new MenuItem("House blend", 0.89));
    cafeMenu.add(new MenuItem("Decaf", 1.05));
    cafeMenu.add(new MenuItem("Dark Roast", 0.99));
    // DessertMenu
    dessertMenu.add(new MenuItem("Apple Pie", 0.95));

    // 创建Menu的客户:Waiter,打印菜单
    CompositeWaiter waiter = new CompositeWaiter(allMenu);
    waiter.printMenu();
}

image-20220211010539194

6.4、外部迭代器

  • 在前面的实现中,我们在 print() 内部使用了迭代器;
  • 现在,我们使用外部迭代器,以供客户使用。

6.4.1、组合迭代器

(Recursion)递归思想:递归遍历迭代器

  1. stack:用一个栈数据结构,来存储所有迭代器;
    • push:添加元素;
    • peek:取出栈顶元素,但不删除;
    • pop:删除栈顶元素。
  2. 构造器:初始化 stack,并将顶级迭代器传入;
  3. hasNext():判断是否有更多元素
    • 空栈:false,说明递归已结束,或没有任何元素;
    • 非空
      1. 取出栈顶元素,即当前的迭代器;
      2. 递归调用 hasNext(),判断当前迭代器是否有跟德国元素
        • 无:删除当前迭代器,返回 false;
        • 有:返回 true;
  4. next():获取下一个元素
    • 调用 hasNext(),判断是否有更多元素;
    • 递归调用 next():取出栈顶元素,即当前的迭代器;
    • 取出迭代器的下一个元素 component 并返回;
      • 若 component 是菜单组合,则 push 到栈中;
      • 下一次调用 hasNext() 方法时,遍历的就是栈顶 component。
public class CompositeIterator implements Iterator {
    private Stack<Iterator<MenuComponent>> stack;

    public CompositeIterator(Iterator<MenuComponent> iterator) {
        stack = new Stack<>();
        stack.push(iterator);
    }

    @Override
    public boolean hasNext() {
        if (stack.empty()) {
            return false;
        }

        Iterator iterator = stack.peek();
        if (!iterator.hasNext()) {
            // 删除栈顶迭代器,递归调用hasNext()
            stack.pop();
            return hasNext();
        }
        return true;
    }

    @Override
    public Object next() {
        if (hasNext()) {
            Iterator<MenuComponent> iterator = stack.peek();

            MenuComponent component = iterator.next();
            if (component instanceof Menu) {
                stack.push(component.createIterator());
            }
            return component;
        }
        return null;
    }
}

6.4.2、空迭代器

提供给 叶节点 MenuItem 使用。相比在 MenuItem 中返回 null,使用空迭代器可以有效避免 NPE

  • hasNext():返回 false;
  • next():返回 null。
public class NullIterator implements Iterator {
    @Override
    public boolean hasNext() {
        return false;
    }

    @Override
    public Object next() {
        return null;
    }
}

6.4.3、MenuComponent

在 MenuComponent 超类及子类中,添加 createIterator() 方法。

  • MenuComponent:默认实现,抛出 UnsupportedOperationException 异常;
  • Menu:返回组合迭代器;
  • MenuItem:返回空迭代器,而不是返回 null(避免 NPE)。
// MenuComponent
public Iterator<MenuComponent> createIterator() {
    throw new UnsupportedOperationException();
}

// Menu
@Override
public Iterator<MenuComponent> createIterator() {
    return new CompositeIterator(componentList.iterator());
}

// MenuItem
@Override
public Iterator<MenuComponent> createIterator() {
    return new NullIterator();
}

7、小结

迭代器模式

迭代器模式:提供一种方法,顺序访问一个聚合对象的各个元素,而不暴露内部表示

  • 单一职责原则:聚合对象将遍历元素的职责交给迭代器,其本身可以专注于管理对象集合;
  • 通常会使用 工厂方法模式 来创建迭代器;
  • 分类
    • 外部迭代器:客户调用 next() 取得下一个元素;
    • 内部迭代器:迭代器自行在元素之间游走。
  • 枚举器(1.0),迭代器(1.2)
  • 增强 for 循环:for/in

组合模式

组合模式将对象组合成树形结构,来表现 “整体/部分” 层次结构。

  • 建立树形结构,包含个别对象和对象组合;
  • 让客户能把相同的操作,应用在个别对象和组合上;
  • 里氏替换原则:继承必须使父类的性质在子类中成立。
  • 配合 迭代器模式使用。
posted @ 2022-02-03 19:08  Jaywee  阅读(53)  评论(0编辑  收藏  举报

👇