GoF23:Iterator & Composite(迭代器&组合)
1、case:菜单
有两份菜单
- 菜单项结构相同;
- 菜单项存储的实现不同:使用不同的“集合”。
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();
}
1.2.4、分析
维护性、扩展性差
Menu
- 违反单一职责原则:既负责存储 Item,又负责遍历工作;
- 针对实现编程:没有设计接口,而是设计两个单独的类。
Waiter
- 代码重复:每个 Menu 都需使用一次循环,而区别仅在于循环上界和获取集合项的方式;
- 针对实现编程:针对具体的 Menu 编程,而不是针对接口编程;
- 违反封装:本类需要知道每个 Menu 的具体实现,以采取相应的遍历方式;
- 违反开闭原则:若 MenuItem 存储方式改变(如 Hashtable),需要修改类的代码。
2、自定义迭代器
- Java API 有内置的迭代器:java.util.Iterator;
- 先实现自定义迭代器,再使用 Java 的迭代器API。
2.1、迭代器:封装遍历
MenuIterator接口
迭代器接口:封装 ”遍历集合的过程“。
- 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 接口
将 Menu 定义成接口,提供 createIterator() 方法:
- 用途:创建迭代器;
- 使用了 工厂方法模式:非参数化工厂方法、只生产一种对象(即迭代器)。
public interface Menu {
MenuIterator createIterator();
}
Menu 实现
改进
- 实现 Menu 接口,实现 createIterator() 方法;
- 去掉 getMenuItems() 方法。
实现
-
PancakeMenu
-
DinerMenu
2.3、Waiter
- 成员变量、构造器参数:声明为接口类型;
- 定义方法 printMenu():通过迭代器遍历集合;
- 原有方法
- 获取菜单集合项 → 获取菜单迭代器;
- 遍历循环体 → 委托 printMenu() 遍历。
2.4、测试
结果完全一样。
@Test
public void test() {
Waiter waiter = new Waiter(new PancakeMenu(), new DinerMenu());
waiter.printPancakeMenu();
waiter.printDinerMenu();
}
2.5、分析
Menu
- 遵循单一职责原则:Menu 负责管理集合,而将遍历集合的职责交给了迭代器。
- 之前:通过 Menu 获取集合,由 Menu 的客户进行遍历;
- 现在:通过 Menu 获取迭代器,由 迭代器 进行遍历;
- 针对接口编程:设计接口,便于客户解耦。
Waiter
- 减少代码重复:只定义一个方法用于遍历集合,将请求委托给该方法;
- 针对接口编程:成员变量、构造器参数,声明为接口类型;
- 封装:Waiter 作为 Menu 的客户,不再需要关注 Menu 集合的实现细节。
3、Java API:迭代器
- 通过以上实例,学会自定义迭代器;
- 接下来,使用 java.util.Iterator 改进本方案。
3.1、迭代器&集合
Iterator
迭代器:java.util.Iterator
相比自定义迭代器,多了两个方法:
- remove():删除最后一个返回的元素;
- forEachRemaining():JDK 1.8 新增。对剩余元素执行给定动作。
Collection
集合:java.util.Collection
- 也称为 聚合(aggregate),指一群对象;
- 可以是各种数据结构:列表、数组、散列表等。
提供了抽象方法 iterator():
-
功能:获取当前集合对象的迭代器;
-
Collection 是一个庞大的集合体系,其子类都实现了 iterator()
3.2、改进
引入 java.util.Iterator 作为迭代器。
3.2.1、迭代器
-
ArrayList
-
具有 iterator() 方法,无需使用自定义迭代器;
-
去除 PancakeMenuIterator 类。
-
-
数组
-
不具有 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();
}
-
PancakeMenu:createIterator() 返回集合的 iterator()
-
DinerMenu:无需修改代码
3.2.3、类图
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();
}
4、迭代器模式
4.1、定义
迭代器模式:提供一种方法,顺序访问一个聚合对象的各个元素,而不暴露内部表示。
-
聚合对象将遍历元素的职责交给迭代器,其本身可以专注于管理对象集合;
-
通常会使用 工厂方法模式 来创建迭代器;
-
OOP原则:单一职责原则;
-
类图
-
内部 & 外部迭代器
- 外部迭代器:客户调用 next() 取得下一个元素;
- 内部迭代器:迭代器自行在元素之间游走。
public interface Menu {
MenuIterator createIterator();
}
4.2、相关机制
枚举器
在集合框架中,迭代器(JDK1.2)取代了枚举器(JDK 1.0)的地位,有以下区别
- 方法名称改善;
- 迭代器支持删除操作,在 JDK1.8 后还引入 forEachRemaining() 方法。
增强for循环
JDK1.5 提供的 for/in 语句,即增强 for 循环。
-
支持在集合或数组中遍历,且不需要显式创建迭代器。
-
语法
- collection:被遍历的集合或数组;
- obj:每次遍历的一个元素。
for (Object obj: collection){ ... }
5、组合模式
定义
组合模式:将对象组合成树形结构,来表现 “整体/部分” 层次结构。
- 建立树形结构,包含个别对象和对象组合;
- 让客户能把相同的操作,应用在个别对象和组合上;
- OOP原则——里氏替换:继承必须使父类的性质在子类中成立。
类图
做法:定义一个抽象组件类,作为个别对象和对象组合的超类。
- Client:针对接口编程,通过 Component 操作组合中的对象;
- Component:组件
- 包含叶节点(Leaf)和组合(Composite);
- 提供一些方法及默认实现:如增加、删除、获取子节点,其它操作元素的行为。
- Composite:组合
- 包含子节点的组件;
- 重写了所有超类方法,定义组件的行为。
- Leaf:叶节点
- 没有子节点的组件;
- 重写了操作元素行为的方法,定义组合内元素的行为;
6、case:子菜单
需求:在 DinerMenu 中加入子菜单:甜品菜单。
6.1、设计
树状结构
将 Menu 体系设计成树状结构
- 组件:菜单,如DinerMenu;
- 叶节点:菜单项,即MenuItem;
- 组合:子菜单,如DessertMenu。
类图
- Waiter:针对接口编程,通过 MenuComponent 操作组合中的对象;
- MenuComponent:组件
- 包含叶节点(MenuItem)和组合(Menu);
- 提供一些方法及默认实现:如增加、删除、获取子节点,其它操作元素的行为。
- Menu:组合,包含子节点;
- Leaf:叶节点,没有子节点;
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、测试
- 创建所有菜单;
- 创建顶级菜单,添加所有菜单;
- 添加菜单项;
- 创建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();
}
6.4、外部迭代器
- 在前面的实现中,我们在 print() 内部使用了迭代器;
- 现在,我们使用外部迭代器,以供客户使用。
6.4.1、组合迭代器
(Recursion)递归思想:递归遍历迭代器
- stack:用一个栈数据结构,来存储所有迭代器;
- push:添加元素;
- peek:取出栈顶元素,但不删除;
- pop:删除栈顶元素。
- 构造器:初始化 stack,并将顶级迭代器传入;
- hasNext():判断是否有更多元素
- 空栈:false,说明递归已结束,或没有任何元素;
- 非空
- 取出栈顶元素,即当前的迭代器;
- 递归调用 hasNext(),判断当前迭代器是否有跟德国元素
- 无:删除当前迭代器,返回 false;
- 有:返回 true;
- 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
组合模式
组合模式:将对象组合成树形结构,来表现 “整体/部分” 层次结构。
- 建立树形结构,包含个别对象和对象组合;
- 让客户能把相同的操作,应用在个别对象和组合上;
- 里氏替换原则:继承必须使父类的性质在子类中成立。
- 配合 迭代器模式使用。