设计模式:行为型
设计模式(Design pattern)代表了最佳实践,通常被有经验的软件开发人员所采用。
一、迭代器 - Iterator#
迭代器使用非常广,它提供了一种顺序访问聚合对象元素的方法,并且不暴露聚合对象的内部表示。JDK 中使用 Iterable 接口来实现迭代功能:
public interface Iterable<T> {
Iterator<T> iterator();
}
Iterable 接口下定义了一个 Iterator()
方法,用来生成一个 Iterator,Iterator 接口主要定义了下面两个方法:
public interface Iterator<E> {
boolean hasNext();
E next();
}
在使用时,先创建一个迭代器,通过 hasNext()
判断其是否还有下一个值并使用 next()
获取。JDK 中的 Collection 继承自 Iterable,所以 Collection 下的实现类都是可迭代的。Map 的迭代通过其下的 KeySet 和 EntrySet 两个 Set 实现,Set 继承自 Collection,所以也是可迭代的。
1.1 叫号器#
运用迭代器可以实现一个简单的叫号器(Queue Management System,QMS),首先模仿 JDK 定义下面两个接口:
interface ManagementSystem<E> {
Caller<E> createCaller();
}
interface Caller<E> {
boolean hasNext();
E next();
}
ManagementSystem 管理系统接口需要定义一个获取叫号器 Caller 的方法,这里 Caller 和 Iterator 功能相同,通过 hasNext()
判断其是否还有下一个号码并使用 next()
获取。
public class QMS implements ManagementSystem<Integer> {
List<Integer> table;
private int curNum;
protected int modCount = 0;
public QMS (int curNum) {
this.curNum = curNum;
table = new ArrayList<Integer>();
for (int i = 1; i <= curNum; i++) {
table.add(i);
}
}
public void addNum() {
modCount++;
table.add(++curNum);
}
@Override
public Caller<Integer> createCaller() {
return new QMSCaller(table, modCount);
}
/*** 内部类 ***/
class QMSCaller implements Caller<Integer> {
private final List<Integer> callerTable;
private int position = 0;
private final int expectedModCount;
public QMSCaller(List<Integer> table, int modCount) {
this.callerTable = table;
this.expectedModCount = modCount;
}
@Override
public boolean hasNext() {
return position < callerTable.size();
}
@Override
public Integer next() {
// 检查是否有改动,有则抛出异常
checkForComodification();
return callerTable.get(position++);
}
private void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
}
先不考虑与 modCount 相关的代码,这个实现其实非常简单,使用当前等待的人数 curNum 初始化 QMS(创建一个 List 存放 curNum 个号码),如果还有人加入,用 addNum()
方法把新号码放入 List 中。开始叫号时,使用 createCaller()
方法创建叫号器 Caller,接下来的叫号工作就交由叫号器 Caller 来完成了。用一个测试类来测试一下:
class QMSTest1 {
public static void main(String[] args) {
QMS qms = new QMS(10);
Caller<Integer> caller = qms.createCaller();
while (caller.hasNext()) {
System.out.print(caller.next() + " ");
}
}
}
1 2 3 4 5 6 7 8 9 10
输出结果正确!
1.2 Fail-Fast#
In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process.
回到刚才的场景,首先我们需要统计总共的票数,然后打开叫号器开始工作。这时候有一个黑客想要插队,他轻松就破解了叫号系统,并且在 QMS 的 table 中 5 号的后面又插入了一个号码:5 号。当原本的 5 号客户结束服务后,叫号系统又叫了一次五号,黑客先生拿着自己用打印机打的 5 号就走了进去。一旁等待的众人十分气愤但又无可奈何……
通常 Iterator 是工作在一个独立的线程中,我们在创建 Iterator 时传入的是数据的浅拷贝,也就是说,如果在 Iterator 工作的同时有另一个线程对数据进行了修改,Iterator 的工作就会出错。Fail-Fast 机制就是用来处理这种情况的。
对于 QMS 中的每一次 addNum()
操作,modCount 都会自增 1;而我们在创建 Caller 时会传入当前的 modCount 并存放在 Caller 下定义的 final 变量 expectedModCount 中。每一次迭代前总是验证 modCount 是否与预期值相同,如果不同就说明原数据已经被篡改,直接抛出异常而不是等待错误发生再处理,正如其名:Fail-Fast。
class QMSTest2 {
public static void main(String[] args) {
QMS qms = new QMS(10);
new Thread( () -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
qms.addNum();
}).start();
new Thread( () -> {
Caller<Integer> caller = qms.createCaller();
while (caller.hasNext()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(caller.next());
}
}).start();
}
}
1 2 3 4 Exception in thread "Thread-1" java.util.ConcurrentModificationException
在第 5 秒时,我们对 QMS 中的数据进行了改动,这导致叫号器报出了异常。
当然,这里只是为了展示迭代器和 Fail-Fast,实际生活中的叫号器不应该是这样的实现,如果我们每次加入新的号码都要处理异常就很不方便了,比如需要处理高并发问题时,“生产者 - 消费者” 模式是不错的选择。