摘要
JDK:1.8.0_202
# 一:场景问题
# 1.1 工资表数据的整合
项目的客户方收购了一家小公司,这家小公司有自己的工资系统,现在需要整合到客户方已有的工资系统上。
客户方已有的工资系统,在内部是采用的List来记录工资列表;而新收购的这家公司的工资系统,在内部是采用的数组来记录工资列表;但是幸运的是,两个系统用来描述工资的数据模型是差不多的。
要整合这两个工资系统的工资数据,当然最简单的方式是考虑直接把新收购的这家公司的工资系统,也改成内部使用List来记录工资列表,但是经过仔细查看源代码,发现有很多的代码跟这个数组相关,还有很多是比较重要的逻辑处理,比如计算工资等,因此只好作罢。
现在除了要把两个工资系统整合起来外,老板还希望能够通过决策辅助系统来统一查看工资数据,他不想看到两份不同的工资表。那么应该如何实现呢?
# 1.2 有何问题
本来就算内部描述形式不一样,只要不需要整合在一起,两个系统单独输出自己的工资表,是没有什么问题的。但是,老板还希望能够以一个统一的方式来查看所有的工资数据,也就是说从外部看起来,两个系统输出的工资表应该是一样的。
经过分析,要满足老板的要求,而且要让两边的系统改动都尽可能的小的话,问题的核心就在于如何能够以一种统一的方式来提供工资数据给决策辅助系统,换句说来说就是:如何能够以一个统一的方式来访问内部实现不同的聚合对象。
# 二:解决方案
用来解决上述问题的一个合理的解决方案就是迭代器模式。
迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示。
所谓聚合是:指一组对象的组合结构,比如:Java中的集合、数组等。
# 2.1 解决思路
仔细分析上面的问题,要以一个统一的方式来访问内部实现不同的聚合对象,那么首先就需要把这个统一的访问方式定义出来,按照这个统一的访问方式定义出来的接口,在迭代器模式中对应的就是Iterator接口。
迭代器迭代的是具体的聚合对象,那么不同的聚合对象就应该有不同的迭代器,为了让迭代器以一个统一的方式来操作聚合对象,因此给所有的聚合对象抽象出一个公共的父类,让它提供操作聚合对象的公共接口,这个抽象的公共父类在迭代器模式中对应的就是Aggregate对象。
接下来就该考虑如何创建迭代器了,由于迭代器和相应的聚合对象紧密相关,因此让具体的聚合对象来负责创建相应的迭代器对象。
- Iterator:迭代器接口。定义访问和遍历元素的接口。
- ConcreteIterator:具体的迭代器实现对象。实现对聚合对象的遍历,并跟踪遍历时的当前位置。
- Aggregate:聚合对象。定义创建相应迭代器对象的接口。
- ConcreteAggregate:具体聚合对象。实现创建相应的迭代器对象。
# 2.3 示例代码
迭代器接口的定义:
/**
* 迭代器接口,定义访问和遍历元素的操作
*/
public interface Iterator {
/**
* 移动到聚合对象的第一个位置
*/
void first();
/**
* 移动到聚合对象的下一个位置
*/
void next();
/**
* 判断是否已经移动到聚合对象的最后一个位置
*
* @return true表示已经移动到聚合对象的最后一个位置,
* false表示还没有移动到聚合对象的最后一个位置
*/
boolean isDone();
/**
* 获取迭代的当前元素
*
* @return 迭代的当前元素
*/
Object currentItem();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
具体的迭代器实现:
/**
* 具体迭代器实现对象,示意的是聚合对象为数组的迭代器
* 不同的聚合对象相应的迭代器实现是不一样的
*/
public class ConcreteIterator implements Iterator {
/**
* 持有被迭代的具体的聚合对象
*/
private ConcreteAggregate aggregate;
/**
* 内部索引,记录当前迭代到的索引位置。
* -1表示刚开始的时候,迭代器指向聚合对象第一个对象之前
*/
private int index = -1;
/**
* 构造方法,传入被迭代的具体的聚合对象
*
* @param aggregate 被迭代的具体的聚合对象
*/
public ConcreteIterator(ConcreteAggregate aggregate) {
this.aggregate = aggregate;
}
public void first() {
index = 0;
}
public void next() {
if (index < this.aggregate.size()) {
index = index + 1;
}
}
public boolean isDone() {
return index == this.aggregate.size();
}
public Object currentItem() {
return this.aggregate.get(index);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
聚合对象的定义:
/**
* 聚合对象的接口,定义创建相应迭代器对象的接口
*/
public abstract class Aggregate {
/**
* 工厂方法,创建相应迭代器对象的接口
*
* @return 相应迭代器对象的接口
*/
public abstract Iterator createIterator();
}
2
3
4
5
6
7
8
9
10
11
12
具体聚合对象的实现:
/**
* 具体的聚合对象,实现创建相应迭代器对象的功能
*/
public class ConcreteAggregate extends Aggregate {
/**
* 示意,表示聚合对象具体的内容
*/
private String[] ss = null;
/**
* 构造方法,传入聚合对象具体的内容
*
* @param ss 聚合对象具体的内容
*/
public ConcreteAggregate(String[] ss) {
this.ss = ss;
}
public Iterator createIterator() {
//实现创建Iterator的工厂方法
return new ConcreteIterator(this);
}
/**
* 获取索引所对应的元素
*
* @param index 索引
* @return 索引所对应的元素
*/
public Object get(int index) {
Object retObj = null;
if (index < ss.length) {
retObj = ss[index];
}
return retObj;
}
/**
* 获取聚合对象的大小
*
* @return 聚合对象的大小
*/
public int size() {
return this.ss.length;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
客户端使用:
public class Client {
public static void main(String[] args) {
String[] names = {"张三", "李四", "王五"};
// 创建聚合对象
Aggregate aggregate = new ConcreteAggregate(names);
// 循环输出聚合对象中的值
Iterator it = aggregate.createIterator();
// 首先设置迭代器到第一个元素
it.first();
while (!it.isDone()) {
// 取出当前的元素来
Object obj = it.currentItem();
System.out.println("the obj==" + obj);
// 如果还没有迭代到最后,那么就向下迭代一个
it.next();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
运行结果:
# 2.4 实现案例-已有系统
工资描述模型:
/**
* 工资描述模型对象
*/
public class PayModel {
/**
* 支付工资的人员
*/
private String userName;
/**
* 支付的工资数额
*/
private double pay;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public double getPay() {
return pay;
}
public void setPay(double pay) {
this.pay = pay;
}
public String toString() {
return "userName=" + userName + ",pay=" + pay;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
客户方工资管理类:
/**
* 客户方已有的工资管理对象
*/
public class PayManager {
/**
* 聚合对象,这里是Java的集合对象
*/
private List<PayModel> list = new ArrayList<>();
/**
* 获取工资列表
*
* @return 工资列表
*/
public List<PayModel> getPayList() {
return list;
}
/**
* 计算工资,其实应该有很多参数,为了演示从简
*/
public void calcPay() {
// 计算工资,并把工资信息填充到工资列表里面
// 为了测试,做点数据进去
PayModel pm1 = new PayModel();
pm1.setPay(3800);
pm1.setUserName("张三");
PayModel pm2 = new PayModel();
pm2.setPay(5800);
pm2.setUserName("李四");
list.add(pm1);
list.add(pm2);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
客户方工资管理类:
/**
* 被客户方收购的那个公司的工资管理类
*/
public class SalaryManager {
/**
* 用数组管理
*/
private PayModel[] pms = null;
/**
* 获取工资列表
*
* @return 工资列表
*/
public PayModel[] getPays() {
return pms;
}
/**
* 计算工资,其实应该有很多参数,为了演示从简
*/
public void calcSalary() {
// 计算工资,并把工资信息填充到工资列表里面
// 为了测试,做点数据进去
PayModel pm1 = new PayModel();
pm1.setPay(2200);
pm1.setUserName("王五");
PayModel pm2 = new PayModel();
pm2.setPay(3600);
pm2.setUserName("赵六");
pms = new PayModel[2];
pms[0] = pm1;
pms[1] = pm2;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
客户端:
public class Client {
public static void main(String[] args) {
// 访问集团的工资列表
PayManager payManager = new PayManager();
// 先计算再获取
payManager.calcPay();
Collection<PayModel> payList = payManager.getPayList();
Iterator<PayModel> it = payList.iterator();
System.out.println("集团工资列表:");
while (it.hasNext()) {
PayModel pm = it.next();
System.out.println(pm);
}
// 访问新收购公司的工资列表
SalaryManager salaryManager = new SalaryManager();
// 先计算再获取
salaryManager.calcSalary();
PayModel[] pms = salaryManager.getPays();
System.out.println("新收购的公司工资列表:");
for (PayModel pm : pms) {
System.out.println(pm);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
运行结果:
# 2.5 实现案例
要使用迭代器模式来整合访问上面两个聚合对象,那就需要先定义出抽象的聚合对象和迭代器接口来,然后再提供相应的实现。
使用迭代器模式实现示例的结构如图所示:
为了让客户端能够以一个统一的方式进行访问,最好想的方式就是为它们定义一个统一的接口,都通过统一的接口来访问就简单了。
public class Client {
public static void main(String[] args) {
// 访问集团的工资列表
PayManager payManager = new PayManager();
// 先计算再获取
payManager.calcPay();
System.out.println("集团工资列表:");
test(payManager.createIterator());
// 访问新收购公司的工资列表
SalaryManager salaryManager = new SalaryManager();
// 先计算再获取
salaryManager.calcSalary();
System.out.println("新收购的公司工资列表:");
test(salaryManager.createIterator());
}
/**
* 测试通过访问聚合对象的迭代器,是否能正常访问聚合对象
*
* @param it 聚合对象的迭代器
*/
private static void test(Iterator it) {
// 循环输出聚合对象中的值
// 首先设置迭代器到第一个元素
it.first();
while (!it.isDone()) {
// 取出当前的元素来
Object obj = it.currentItem();
System.out.println("the obj==" + obj);
// 如果还没有迭代到最后,那么就向下迭代一个
it.next();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
接口实现:(ArrayIteratorImpl.java 和 CollectionIteratorImpl.java)
/**
* 用来实现访问数组的迭代接口
*/
public class ArrayIteratorImpl implements Iterator {
/**
* 用来存放被迭代的聚合对象
*/
private SalaryManager aggregate = null;
/**
* 用来记录当前迭代到的位置索引
* -1表示刚开始的时候,迭代器指向聚合对象第一个对象之前
*/
private int index = -1;
public ArrayIteratorImpl(SalaryManager aggregate) {
this.aggregate = aggregate;
}
public void first() {
index = 0;
}
public void next() {
if (index < this.aggregate.size()) {
index = index + 1;
}
}
public boolean isDone() {
return index == this.aggregate.size();
}
public Object currentItem() {
return this.aggregate.get(index);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 用来实现访问Collection集合的迭代接口,为了外部统一访问方式
*/
public class CollectionIteratorImpl implements Iterator {
/**
* 用来存放被迭代的聚合对象
*/
private PayManager aggregate = null;
/**
* 用来记录当前迭代到的位置索引
* -1表示刚开始的时候,迭代器指向聚合对象第一个对象之前
*/
private int index = -1;
public CollectionIteratorImpl(PayManager aggregate) {
this.aggregate = aggregate;
}
public void first() {
index = 0;
}
public void next() {
if (index < this.aggregate.size()) {
index = index + 1;
}
}
public boolean isDone() {
return index == this.aggregate.size();
}
public Object currentItem() {
return this.aggregate.get(index);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
获取访问聚合的接口:
定义好了统一的访问聚合的接口,也分别实现了这个接口,新的问题是,在客户端要如何才能获取这个访问聚合的接口呢?而且还要以统一的方式来获取。
一个简单的方案就是定义一个获取访问聚合的接口的接口,客户端先通过这个接口来获取访问聚合的接口,然后再访问聚合对象。
public abstract class Aggregate {
/**
* 工厂方法,创建相应迭代器对象的接口
*
* @return 相应迭代器对象的接口
*/
public abstract Iterator createIterator();
}
2
3
4
5
6
7
然后让具体的聚合对象PayManger和SalaryManager来继承这个抽象类,提供分别访问它们的访问聚合的接口。
修改PayManager对象,添加createIterator方法的实现,另外再添加迭代器回调聚合对象的方法,一个方法是获取聚合对象的大小,一个方法是根据索引获取聚合对象中的元素。
public class PayManager extends Aggregate {
private List<PayModel> list = new ArrayList<>();
public Iterator createIterator() {
return new CollectionIteratorImpl(this);
}
public PayModel get(int index) {
PayModel retObj = null;
if (index < this.list.size()) {
retObj = this.list.get(index);
}
return retObj;
}
public int size() {
return this.list.size();
}
/**
* 获取工资列表
*
* @return 工资列表
*/
public List<PayModel> getPayList() {
return list;
}
/**
* 计算工资,其实应该有很多参数,为了演示从简
*/
public void calcPay() {
// 计算工资,并把工资信息填充到工资列表里面
// 为了测试,做点数据进去
PayModel pm1 = new PayModel();
pm1.setPay(3800);
pm1.setUserName("张三");
PayModel pm2 = new PayModel();
pm2.setPay(5800);
pm2.setUserName("李四");
list.add(pm1);
list.add(pm2);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
同理修改SalaryManager对象
public class SalaryManager extends Aggregate {
private PayModel[] pms;
public Iterator createIterator() {
return new ArrayIteratorImpl(this);
}
public PayModel get(int index) {
PayModel retObj = null;
if (index < pms.length) {
retObj = pms[index];
}
return retObj;
}
public int size() {
return this.pms.length;
}
/**
* 获取工资列表
*
* @return 工资列表
*/
public PayModel[] getPays() {
return pms;
}
/**
* 计算工资,其实应该有很多参数,为了演示从简
*/
public void calcSalary() {
// 计算工资,并把工资信息填充到工资列表里面
// 为了测试,做点数据进去
PayModel pm1 = new PayModel();
pm1.setPay(2200);
pm1.setUserName("王五");
PayModel pm2 = new PayModel();
pm2.setPay(3600);
pm2.setUserName("赵六");
pms = new PayModel[2];
pms[0] = pm1;
pms[1] = pm2;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
客户端:
public class Client {
public static void main(String[] args) {
// 访问集团的工资列表
PayManager payManager = new PayManager();
// 先计算再获取
payManager.calcPay();
System.out.println("集团工资列表:");
test(payManager.createIterator());
// 访问新收购公司的工资列表
SalaryManager salaryManager = new SalaryManager();
// 先计算再获取
salaryManager.calcSalary();
System.out.println("新收购的公司工资列表:");
test(salaryManager.createIterator());
}
/**
* 测试通过访问聚合对象的迭代器,是否能正常访问聚合对象
*
* @param it 聚合对象的迭代器
*/
private static void test(Iterator it) {
// 循环输出聚合对象中的值
// 首先设置迭代器到第一个元素
it.first();
while (!it.isDone()) {
// 取出当前的元素来
Object obj = it.currentItem();
System.out.println("the obj==" + obj);
// 如果还没有迭代到最后,那么就向下迭代一个
it.next();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
运行结果:
# 2.6 示例小结
如同前面示例,提供了一个统一访问聚合对象的接口,通过这个接口就可以顺序的访问聚合对象的元素,对于客户端而言,只是面向这个接口在访问,根本不知道聚合对象内部的表示方法。
事实上,前面的例子故意做了一个集合类型的聚合对象和一个数组类型的聚合对象,但是从客户端来看,访问聚合的代码是完全一样的,根本看不出任何的差别,也看不出到底聚合对象内部是什么类型。
# 三:模式讲解
# 3.1 认识迭代器模式
1. 迭代器模式的功能
迭代器模式的功能主要在于提供对聚合对象的迭代访问。迭代器就围绕着这个 "访问" 做文章,延伸出很多的功能来。比如:
以不同的方式遍历聚合对象,比如向前、向后等;
对同一个聚合同时进行多个遍历;
以不同的遍历策略来遍历聚合,比如是否需要过滤等;
多态迭代,含义是:为不同的聚合结构,提供统一的迭代接口,也就是说通过一个迭代接口可以访问不同的聚合结构,这就叫做多态迭代。上面的示例就已经实现了多态迭代,事实上,标准的迭代模式实现基本上都是支持多态迭代的;
但是请注意:多态迭代可能会带来类型安全的问题,可以考虑使用泛型;
2. 迭代器模式的关键思想
聚合对象的类型很多,如果对聚合对象的迭代访问跟聚合对象本身融合在一起的话,会严重影响到聚合对象的可扩展性和可维护性。
因此迭代器模式的关键思想就是把对聚合对象的遍历和访问从聚合对象中分离出来,放入到单独的迭代器中,这样聚合对象会变得简单一些;而且迭代器和聚合对象可以独立的变化和发展,会大大加强系统的灵活性。
3. 内部迭代器和外部迭代器
所谓内部迭代器,指的是由迭代器自己来控制迭代下一个元素的步骤,客户端无法干预,因此,如果想要在迭代的过程中完成工作的话,客户端就需要把操作传给迭代器,迭代器在迭代的时候会在每个元素上执行这个操作,类似于Java的回调机制。
所谓外部迭代器,指的是由客户端来控制迭代下一个元素的步骤,像前面的示例一样,客户端必须显示的调用next来迭代下一个元素。
总体来说外部迭代器比内部迭代器要灵活一些,因此我们常见的实现多属于外部迭代器,前面的例子也是实现的外部迭代器。
# 3.2 优缺点
1. 更好的封装性
迭代器模式可以让你访问一个聚合对象的内容,而无需暴露该聚合对象的内部表示,从而提高聚合对象的封装性;
2. 可以以不同的遍历方式来遍历一个聚合
使用迭代器模式,使得聚合对象的内容和具体的迭代算法分离开,这样就可以通过使用不同的迭代器的实例,就可以使用不同的遍历方式来遍历一个聚合对象了,比如上面示例的带迭代策略的迭代器。
3. 迭代器简化了聚合的接口
有了迭代器的接口,那么聚合本身就不需要再定义这些接口了,从而简化了聚合的接口定义。
4. 简化客户端调用
迭代器为遍历不同的聚合对象提供了一个统一的接口,使得客户端遍历聚合对象的内容变得更简单
5. 同一个聚合上可以有多个遍历
每个迭代器保持它自己的遍历状态,比如前面实现中的迭代索引位置,因此可以对同一个聚合对象同时进行多个遍历。
# 3.3 思考迭代器模式
1. 迭代器模式的本质
迭代器模式的本质:控制访问聚合对象中的元素。
迭代器能实现 "无需暴露聚合对象的内部实现,就能够访问到聚合对象中各个元素" 的功能,看起来其本质应该是 "透明访问聚合对象中的元素"。
但仔细思考一下,除了透明外,迭代器就没有别的功能了吗?很明显还有其它的功能,前面也讲到了一些,比如“带迭代策略的迭代器”。那么综合来看,迭代器模式的本质应该是“控制访问聚合对象中的元素”,而非单纯的“透明”,事实上,“透明”访问也是“控制访问”的一种情况。
认识这个本质,对于识别和变形使用迭代器模式很有帮助。大家想想,现在的迭代模式默认的都是向前或者向后获取一个值,也就是说都是单步迭代,那么,如果想要控制一次迭代多条怎么办呢?
这个在实际开发中是很有用的,比如在实际开发中很常用的翻页功能的实现,常见的翻页功能有如下几种实现方式:
纯数据库实现:依靠SQL提供的功能实现翻页,用户每次请求翻页的数据,就会到数据库中获取相应的数据;
纯内存实现:就是一次性从数据库中把需要的所有数据都取出来放到内存中,然后用户请求翻页时,从内存中获取相应的数据;
面两种方式各有优缺点:
- 第一种方案明显是时间换空间的策略,每次获取翻页的数据都要访问数据库,运行速度相对比较慢,而且很耗数据库资源,但是节省内存空间。
- 第二种方案是典型的空间换时间,每次是直接从内存中获取翻页的数据,运行速度快,但是很耗内存。
- 在实际开发中,小型系统一般采用第一种方案,基本没有单独采用第二种方案的,因为内存实在是太宝贵了,中大型的系统一般是把两个方案结合起来,综合利用它们的优点,而又规避它们的缺点,从而更好的实现翻页的功能。
纯数据库实现 + 纯内存实现,思路是这样的:如果每页显示10条记录,根据判断,用户很少翻到10页过后,那好了,第一次访问的时候,就一次性从数据库中获取前10页的数据,也就是100条记录,把这100条记录放在内存里面。
这样一来,当用户在前10页内进行翻页操作的时候,就不用再访问数据库了,而是直接从内存中获取数据,这样速度就快了。
当用户想要获取第11页的数据,这个时候才会再次访问数据库,对于这个时候到底获取多少页的数据,简单的处理就是继续获取10页的数据,比较好的方式就是根据访问统计进行衰减访问,比如折半获取,也就是第一次访问数据库获取10页的数据,那么第二次就只获取5页,如此操作直到一次从数据库中获取一页的数据。这也符合正常规律,因为越到后面,被用户翻页到的机会也就越小了。
2. 何时选用迭代器模式
建议在如下情况中,选用迭代器模式:
如果你希望提供访问一个聚合对象的内容,但是又不想暴露它的内部表示的时候,可以使用迭代器模式来提供迭代器接口,从而让客户端只是通过迭代器的接口来访问聚合对象,而无需关心聚合对象内部实现。
如果你希望有多种遍历方式可以访问聚合对象,可以使用迭代器模式。
如果你希望为遍历不同的聚合对象提供一个统一的接口,可以使用迭代器模式。
# 3.4 相关模式
1. 迭代器模式和组合模式
这两个模式可以组合使用。
组合模式是一种递归的对象结构,在枚举某个组合对象的子对象的时候,通常会使用迭代器模式。
2. 迭代器模式和工厂方法模式
这两个模式可以组合使用。
在聚合对象创建迭代器的时候,通常会采用工厂方法模式来实例化相应的迭代器对象。