设计模式:行为型

设计模式(Design pattern)代表了最佳实践,通常被有经验的软件开发人员所采用。

一、迭代器 - Iterator#

迭代器使用非常广,它提供了一种顺序访问聚合对象元素的方法,并且不暴露聚合对象的内部表示。JDK 中使用 Iterable 接口来实现迭代功能:

public interface Iterable<T> {
    
    Iterator<T> iterator();
}

Iterable 接口下定义了一个 Iterator() 方法,用来生成一个 IteratorIterator 接口主要定义了下面两个方法:

public interface Iterator<E> {

    boolean hasNext();

    E next();
}

在使用时,先创建一个迭代器,通过 hasNext() 判断其是否还有下一个值并使用 next() 获取。JDK 中的 Collection 继承自 Iterable,所以 Collection 下的实现类都是可迭代的。Map 的迭代通过其下的 KeySetEntrySet 两个 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 的方法,这里 CallerIterator 功能相同,通过 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.

回到刚才的场景,首先我们需要统计总共的票数,然后打开叫号器开始工作。这时候有一个黑客想要插队,他轻松就破解了叫号系统,并且在 QMStable 中 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,实际生活中的叫号器不应该是这样的实现,如果我们每次加入新的号码都要处理异常就很不方便了,比如需要处理高并发问题时,“生产者 - 消费者” 模式是不错的选择。

未完待续…

2019-2021 © lil-q