迭代器模式
定义
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示
迭代器模式我们很熟悉,其实就是Java中集合的迭代原理,如果你研究过集合的源码,就会很容易理解这个模式。
场景+代码
场景
现在有两家餐厅:煎饼店和饭店,它们想要合并。两家店的菜单需要统一起来,以便服务员给客户看。
但此时有一个问题,煎饼店和饭店的菜单,实现是不一样的:
/**
* 菜单项
*/
public class MenuItem {
String name;
double price;
public MenuItem() {}
public MenuItem(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
/**
* 煎饼菜单
*/
public class PancakeMenu {
ArrayList<MenuItem> menuItems = new ArrayList<>();
public void addItem(String name,double price){
MenuItem menuItem = new MenuItem(name, price);
menuItems.add(menuItem);
}
public PancakeMenu() {
initItems();
}
public void initItems(){
addItem("葱花饼", 3.5);
addItem("牛肉饼", 6);
addItem("猪肉饼", 5);
addItem("韭菜饼", 2.5);
}
}
/**
* 午餐菜单
*/
public class DinnerMenu {
MenuItem[] menuItems = new MenuItem[20];
int numberOfItems = 0;
public DinnerMenu() {
initItems();
}
public void addItem(String name,double price){
MenuItem menuItem = new MenuItem(name, price);
if(numberOfItems >= menuItems.length){
System.out.println("菜单已满,不能再添加!");
return ;
}
menuItems[numberOfItems] = menuItem;
numberOfItems++;
}
public Iterator<MenuItem> createIterator() {
return new DinnerMenuIterator(menuItems);
}
public void initItems(){
addItem("青椒肉丝炒饭", 12);
addItem("番茄鸡蛋炒饭", 10);
addItem("鱼香茄子炒饭", 11);
addItem("土豆烧鸡炒饭", 12.5);
}
}
从代码我们可以看出,两家店的菜单一个是数组实现,一个是集合实现。
这时,我们想要服务员提供新的菜单时,得分别针对两种不同的底层实现,进行不同的迭代:
/**
* 打印出菜单
*/
public void printMenu() {
ArrayList<MenuItem> pMenuItems = pancakeMenu.menuItems;
for (int i = 0; i < pMenuItems.size(); i++) {
MenuItem menuItem = pMenuItems.get(i);
System.out.println(menuItem.getName()+":"+menuItem.getPrice());
}
MenuItem[] dMenuItems = dinnerMenu.menuItems;
for (int i = 0,n = dMenuItems.length; i < n; i++) {
MenuItem menuItem = dMenuItems[i];
System.out.println(menuItem.getName()+":"+menuItem.getPrice());
}
}
这个代码产生了几个问题:
- 破坏了封装,暴露了菜单的实现细节
- 扩展性不好,如果现在有一家新的咖啡店加盟,它的菜单实现是
HashMap
,那又得重新增加新的代码。 - 迭代的部分实际上大同小异,我们一直在进行“重复性”的代码
如何改进?让我们思考一下之前学过的设计原则:
找出应用中可能需要变化之处,把他们独立出来。
每个菜单的迭代过程是不一样的,那我们可以将菜单的迭代动作封装出来呀。这就是迭代器模式
类图
我们先来看一下该模式的类图:
迭代动作被封装成了一个接口Iterator
,每一种聚合(也就是不同底层实现的集合),都对应了一种具体的迭代器ConcreteIterator
。
实现
接下来我们结合场景来写出迭代器模式的详细代码:
/**
* 迭代器接口
*/
public interface Iterator<E> {
public boolean hashNext();
public E next();
}
/**
* 饭店菜单迭代器
*/
public class DinnerMenuIterator implements Iterator<MenuItem>{
MenuItem[] menuItems;
int position = 0;
public DinnerMenuIterator(MenuItem[] menuItems) {
this.menuItems = menuItems;
}
@Override
public boolean hashNext() {
if(position >= menuItems.length || menuItems[position] == null)
return false;
return true;
}
@Override
public MenuItem next() {
MenuItem item = menuItems[position];
position += 1;
return item;
}
}
/**
* 煎饼菜单迭代器
*/
public class PancakeMenuIterator implements Iterator<MenuItem>{
ArrayList<MenuItem> menuItems;
int position = 0;
public PancakeMenuIterator(ArrayList<MenuItem> menuItems) {
this.menuItems = menuItems;
}
@Override
public boolean hashNext() {
if(position >= menuItems.size() || menuItems.get(position) == null)
return false;
return true;
}
@Override
public MenuItem next() {
MenuItem item = menuItems.get(position);
position += 1;
return item;
}
}
定义了两种菜单迭代器后,我们只需要把迭代器组合进对应的菜单,即可让菜单拥有对应的迭代功能
/**
* 煎饼菜单
*/
public class PancakeMenu {
ArrayList<MenuItem> menuItems = new ArrayList<>();
public void addItem(String name,double price){
MenuItem menuItem = new MenuItem(name, price);
menuItems.add(menuItem);
}
public PancakeMenu() {
initItems();
}
/**
* 创建自己的迭代器
*/
public Iterator<MenuItem> createIterator() {
return new PancakeMenuIterator(menuItems);
}
public void initItems(){
addItem("葱花饼", 3.5);
addItem("牛肉饼", 6);
addItem("猪肉饼", 5);
addItem("韭菜饼", 2.5);
}
}
/**
* 午餐菜单
*/
public class DinnerMenu {
MenuItem[] menuItems = new MenuItem[20];
int numberOfItems = 0;
public DinnerMenu() {
initItems();
}
public void addItem(String name,double price){
MenuItem menuItem = new MenuItem(name, price);
if(numberOfItems >= menuItems.length){
System.out.println("菜单已满,不能再添加!");
return ;
}
menuItems[numberOfItems] = menuItem;
numberOfItems++;
}
/**
* 创建自己的迭代器
*/
public Iterator<MenuItem> createIterator() {
return new DinnerMenuIterator(menuItems);
}
public void initItems(){
addItem("青椒肉丝炒饭", 12);
addItem("番茄鸡蛋炒饭", 10);
addItem("鱼香茄子炒饭", 11);
addItem("土豆烧鸡炒饭", 12.5);
}
}
现在的服务员,不需要清楚菜单的具体设计,打印总菜单时,只需要调用各自菜单的迭代器即可。而且即使扩展增加新的菜单时,也更加容易:
/**
* 服务员
*/
public class Waitress {
PancakeMenu pancakeMenu = new PancakeMenu();
DinnerMenu dinnerMenu = new DinnerMenu();
/**
* 打印出菜单
*/
public void printMenu() {
Iterator<MenuItem> iterator = pancakeMenu.createIterator();
printMenu(iterator);
iterator = dinnerMenu.createIterator();
printMenu(iterator);
}
private void printMenu(Iterator<MenuItem> iterator){
while(iterator.hashNext()){
MenuItem menuItem = iterator.next();
System.out.println(menuItem.getName()+":"+menuItem.getPrice());
}
}
}
新设计原则-单一责任原则
看到这里,有人可能会问,为什么要把迭代的功能抽离成一个接口、还搞组合这么麻烦呢?我们可以直接在各种菜单里面,加上自己的菜单迭代方法就行了呀。
这样同样不会破坏封装,服务员打印订单时调用菜单的打印方法,不会暴露出菜单的内部具体细节。但是,这样做,有两个不好的地方:
- 每个菜单都要实现各自的迭代方法,如果新加盟店的菜单实现和已经合并的店一致,那又将造成重复性代码
- 违法了一个设计原则:单一责任原则
我们先看一下迭代器模式的优点:
迭代器模式能够不暴露聚合内部的具体实现,而且也让聚合任务减轻,把“游走”的任务放在了迭代器上,简化了聚合的接口和实现,也让任务各得其所。
加粗部分就是单一责任原则的体现。我们现在来正式看一下定义:
单一责任原则:一个类应该只有一个引起变化的原因
之所以我们要让一个类只有一个改变的原因,在于:我们需要避免类的改变,而类的责任越多时,它改变的机率就越大。而且,当类真的改变时,两个责任的代码都可能受到影响。
组合模式
定义
组合模式允许你将对象组合成树形结构来表示“整体/部分”层次机构。组合能让客户以一致的方式处理个别对象以及对象组合
当你有数个对象的集合,它们彼此直接有“整体/部分”的关系,而且你想用一致的方式来对待这些对象时,就需要用到组合模式。组合模式通常是用树形结构。
场景+代码
场景
还是采用上面的菜单场景。
首先,细心的朋友会发现上面的服务员代码仍有些不完美的地方:
/**
* 服务员
*/
public class Waitress {
PancakeMenu pancakeMenu = new PancakeMenu();
DinnerMenu dinnerMenu = new DinnerMenu();
/**
* 打印出菜单
*/
public void printMenu() {
Iterator<MenuItem> iterator = pancakeMenu.createIterator();
printMenu(iterator);
iterator = dinnerMenu.createIterator();
printMenu(iterator);
}
……
}
菜单没有被统一管理起来,我们定义了两个菜单成员变量,调用了两次printMenu
方法。当菜单增多时,这个数目还得继续增加……
思考一下,我们可以设计一个菜单父类接口,让现有的煎饼菜单和正餐菜单实现这个接口,然后服务员只需要拥有一个抽象菜单类的数组,在打印菜单时遍历数组,使用元素的迭代器就可以了。
public interface Menu {
public abstract void addItem(String name,double price);
public abstract Iterator<MenuItem> createIterator();
}
public class Waitress {
List<Menu> menus = new ArrayList<>();
/**
* 打印出菜单
*/
public void printMenu() {
for (Menu menu : menus) {
Iterator<MenuItem> iterator = menu.createIterator();
printMenu(iterator);
}
}
private void printMenu(Iterator<MenuItem> iterator){
while(iterator.hashNext()){
MenuItem menuItem = iterator.next();
System.out.println(menuItem.getName()+":"+menuItem.getPrice());
}
}
}
看上去很完美,但不幸,现在来了新的需求:给正餐菜单增加“甜点”子菜单。如图,让甜点菜单变成正餐菜单中的一个子节点,我们想要的类似下图。但很明显,现在的设计无法满足:
类图
怎么办?这个时候我们就要采用组合模式了。我们先看看它的类图:
我们需要某种树状结构来容纳嵌套菜单和菜单项,正如类图所似。
代码
根据类图,我们设计出一个Component
抽象类,让菜单(Composite
)和菜单项(Leaf
)继承它:
public abstract class MenuComponent {
//=======菜单(组合节点)的方法=======
/**
* 增加菜单项
*/
public void add(MenuComponent component) {
throw new UnsupportedOperationException();
}
/**
* 删除指定菜单项
*/
public void remove(MenuComponent component) {
throw new UnsupportedOperationException();
}
/**
* 获取指定下标的菜单项
*/
public MenuComponent get(int i) {
throw new UnsupportedOperationException();
}
//=======菜单项(叶子节点)的方法=======
/**
* 获取菜单项的名称
*/
public String getName() {
throw new UnsupportedOperationException();
}
/**
* 获取菜单项的价格
*/
public double getPrice() {
throw new UnsupportedOperationException();
}
/**
* 打印出菜单中的所有菜单项的信息,或者单个菜单项的信息
* 菜单和菜单项都要用到的方法
*/
public void print(){
}
}
/**
* 组合菜单
*/
public class Menu extends MenuComponent{
List<MenuComponent> menuComponents = new ArrayList<>();
String name;
String description;
public Menu() {}
public Menu(String name,String description) {
this.name = name;
this.description = description;
}
@Override
public void add(MenuComponent component) {
menuComponents.add(component);
}
@Override
public void remove(MenuComponent component) {
menuComponents.remove(component);
}
@Override
public MenuComponent get(int i) {
return menuComponents.get(i);
}
@Override
public void print() {
System.out.println("菜单名称:"+getName()+"("+getDescription()+")");
for (MenuComponent menuComponent : menuComponents) {
menuComponent.print();
}
}
@Override
public String getName() {
return this.name;
}
public String getDescription() {
return description;
}
}
/**
* 菜单项
*/
public class MenuItem extends MenuComponent{
String name;
double price;
public MenuItem() {}
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(name+":"+price);
}
public void setName(String name) {
this.name = name;
}
public void setPrice(double price) {
this.price = price;
}
}
要注意的是,抽象父类中,我们同时将菜单项和菜单的方法定义了出来,继承时,菜单项和菜单只需要覆盖各自的实现即可。这很明显违法了我们上面提到的“单一责任”设计原则,但在具体场景下,为了需求和设计,我们必须采用折中的方式。
结合新的“组合”菜单设计,服务员现在变得很幸福,我们来看看服务员的新代码:
/**
* 服务员
*/
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
/**
* 打印出菜单
*/
public void printMenu() {
allMenus.print();
}
}
测试:
public class Main {
public static void main(String[] args) {
//建造烧饼菜单
MenuComponent pancakeMenu = new Menu("早餐","原烧饼店菜单");
pancakeMenu.add(new MenuItem("葱花饼", 3.5));
pancakeMenu.add(new MenuItem("牛肉饼", 6));
pancakeMenu.add(new MenuItem("猪肉饼", 5));
pancakeMenu.add(new MenuItem("韭菜饼", 2.5));
//建造正餐菜单子菜单-甜点菜单
MenuComponent dessertMenu = new Menu("甜点","正餐子菜单");
dessertMenu.add(new MenuItem("芒果牛奶冰", 8));
dessertMenu.add(new MenuItem("珍珠果豆花", 6));
dessertMenu.add(new MenuItem("榴莲蛋黄", 8));
//建造正餐菜单
MenuComponent dinnerMenu = new Menu("正餐","原饭店菜单");
dinnerMenu.add(new MenuItem("青椒肉丝炒饭", 12));
dinnerMenu.add(new MenuItem("番茄鸡蛋炒饭", 10));
dinnerMenu.add(new MenuItem("鱼香茄子炒饭", 11));
dinnerMenu.add(new MenuItem("土豆烧鸡炒饭", 12.5));
dinnerMenu.add(dessertMenu);
//建造主菜单
MenuComponent totalMenu = new Menu("菜单", "总菜单");
totalMenu.add(pancakeMenu);
totalMenu.add(dinnerMenu);
//服务员打印菜单
Waitress waitress = new Waitress(totalMenu);
waitress.printMenu();
}
}/**Output:
菜单名称:菜单(总菜单)
菜单名称:早餐(原烧饼店菜单)
葱花饼:3.5
牛肉饼:6.0
猪肉饼:5.0
韭菜饼:2.5
菜单名称:正餐(原饭店菜单)
青椒肉丝炒饭:12.0
番茄鸡蛋炒饭:10.0
鱼香茄子炒饭:11.0
土豆烧鸡炒饭:12.5
菜单名称:甜点(正餐子菜单)
芒果牛奶冰:8.0
珍珠果豆花:6.0
榴莲蛋黄:8.0
*/
扩展:组合迭代器
我们现在再扩展一下,这种组合菜单如何设计迭代器呢?细心的朋友应该观察到,我们刚才使用的迭代都是递归调用的菜单项和菜单内部迭代的方式。
现在我们想设计一个外部迭代的方式怎么办?譬如出现一个新需求:服务员需要打印出蔬菜性质的所有食品菜单。
首先,我们给MenuComponent
加上判断蔬菜类食品的方法,然后在菜单项中进行重写:
public abstract class MenuComponent {
…………
/**
* 判断是否为蔬菜类食品
*/
public boolean isVegetarian() {
throw new UnsupportedOperationException();
}
}
/**
* 菜单项
*/
public class MenuItem extends MenuComponent{
String name;
double price;
/**蔬菜类食品标志*/
boolean vegetarian;
…………
public boolean isVegetarian() {
return vegetarian;
}
public void setVegetarian(boolean vegetarian) {
this.vegetarian = vegetarian;
}
}
然后我们设计一个新的组合菜单迭代器。注意,这个迭代器中使用了递归的算法,弄懂递归后理解代码就不难:
public class CompositeIterator implements Iterator<MenuComponent>{
/**迭代器栈,存储最近一次迭代的菜单的迭代器*/
Stack<Iterator<MenuComponent>> stack = new Stack<>();
public CompositeIterator() {}
public CompositeIterator(Iterator<MenuComponent> iterator) {
//初始化时中将组合菜单的总迭代器(即List<MenuComponent>的迭代器)放入栈
stack.push(iterator);
}
@Override
public boolean hasNext() {
if(stack.isEmpty())
return false;
//返回栈顶的迭代器
Iterator<MenuComponent> iterator = stack.peek();
//如果该迭代器仍有元素可迭代,返回true
if(iterator.hasNext()){
return true;
}
//如果迭代器元素都已迭代完,弹出栈,递归判断栈中下一个迭代器
else {
stack.pop();
return hasNext();
}
}
@Override
public MenuComponent next() {
//如果没有元素可迭代,直接返回null
if(!hasNext())
return null;
//有元素可迭代时,返回栈顶迭代器
Iterator<MenuComponent> iterator = stack.peek();
//取出迭代器中的下一个元素
MenuComponent component = iterator.next();
//如果该元素为菜单,将子菜单的迭代器放入栈中。这样下一次执行next()方法,则迭代该子菜单了
if(component instanceof Menu){
stack.push(component.createIterator());
}
return component;
}
}
再在组合菜单中增加创建迭代器的方法:
public abstract class MenuComponent {
…………
/**
* 创建迭代器
* @return
*/
public abstract Iterator<MenuComponent> createIterator();
}
/**
* 菜单项
*/
public class MenuItem extends MenuComponent{
…………
@Override
public Iterator<MenuComponent> createIterator() {
return new NullIterator();
}
}
/**
* 组合菜单
*/
public class Menu extends MenuComponent{
…………
@Override
public Iterator<MenuComponent> createIterator() {
return new CompositeIterator(menuComponents.iterator());
}
}
注意,NullIterator
是之前讲过的“空对象”设计思想的一种体现:
/**
* 空迭代器,空对象思想的体现
*/
public class NullIterator implements Iterator<MenuComponent>{
@Override
public boolean hasNext() {
return false;
}
@Override
public MenuComponent next() {
return null;
}
}
最后,我们给服务员增加新的打印蔬菜食品菜单的方法:
/**
* 打印蔬菜食品
*/
public void printVegetarianMenu() {
Iterator<MenuComponent> iterator = allMenus.createIterator();
while (iterator.hasNext()) {
MenuComponent component = iterator.next();
try{
if(component.isVegetarian())
System.out.println(component.getName());
}
//如果为菜单调用isVegetarian方法,会直接抛出异常,这里会直接不处理
catch (UnsupportedOperationException e) {}
}
}
测试:
public class Main {
public static void main(String[] args) {
//建造烧饼菜单
MenuComponent pancakeMenu = new Menu("早餐","原烧饼店菜单");
pancakeMenu.add(new MenuItem("葱花饼", 3.5,true));
pancakeMenu.add(new MenuItem("牛肉饼", 6,false));
pancakeMenu.add(new MenuItem("猪肉饼", 5,false));
pancakeMenu.add(new MenuItem("韭菜饼", 2.5,true));
//建造正餐菜单子菜单-甜点菜单
MenuComponent dessertMenu = new Menu("甜点","正餐子菜单");
dessertMenu.add(new MenuItem("芒果牛奶冰", 8,false));
dessertMenu.add(new MenuItem("珍珠果豆花", 6,false));
dessertMenu.add(new MenuItem("榴莲蛋黄", 8,false));
//建造正餐菜单
MenuComponent dinnerMenu = new Menu("正餐","原饭店菜单");
dinnerMenu.add(new MenuItem("青椒肉丝炒饭", 12,false));
dinnerMenu.add(new MenuItem("番茄鸡蛋炒饭", 10,true));
dinnerMenu.add(new MenuItem("鱼香茄子炒饭", 11,false));
dinnerMenu.add(new MenuItem("土豆烧鸡炒饭", 12.5,false));
dinnerMenu.add(dessertMenu);
//建造主菜单
MenuComponent totalMenu = new Menu("菜单", "总菜单");
totalMenu.add(pancakeMenu);
totalMenu.add(dinnerMenu);
//服务员打印菜单
Waitress waitress = new Waitress(totalMenu);
//waitress.printMenu();
waitress.printVegetarianMenu();
}
}/**Output:
葱花饼
韭菜饼
番茄鸡蛋炒饭
*/
组合模式的迭代器就设计好啦~~
本文总结自:
《Head First 设计模式》第九章:迭代器和组合模式