11种行为型模式(下):观察者、备忘录、访问者、状态、解释器

观察者模式 Observer

也叫发布订阅模式 Publish/Subscribe。定义对象间一对多依赖关系,依赖对象可以得到通知并自动更新。包含一下几个角色:

  1. Subject 被观察者:一般是抽象类或实现类,能动态增删观察者,并完成管理观察者并通知观察者的职责。
  2. Observer 观察者:观察者接收消息后进行处理。
  3. ConcreteSubject 具体的被观察者:定义个性化业务逻辑,同事定义对哪些事件进行通知。
  4. ConcreteObserver 具体的观察者:定义个性化处理逻辑。

类图

使用场景

  1. 关联行为场景。注意关联行为可拆分,而不是 “组合” 关系。
  2. 事件多级触发场景。
  3. 跨系统的消息交换场景,如消息队列的处理机制。
  4. 实践中的应用:
    • 文件系统:比如在一个目录下新建一个文件,这个动作会同时通知目录管理器增加该目录,并通知磁盘管理器减少 1kb 的空间,也就是说 “文件” 时一个被观察者,“目录管理器” 和 “磁盘管理器” 则是观察者。
    • 猫鼠游戏:夜里猫叫一声,家里的老鼠逃跑,同时也吵醒了熟睡的主人。
    • ATM 取钱:多次输错密码会被吞卡,此时触发的动作有:1. 摄像头连续快拍;2. 通知监控系统发生吞卡; 3. 初始化 ATM 机屏幕,返回最初状态。通常 1、2 通过观察者模式来完成,3 异常完成。
    • 广播收音机:电台广播,可用多个收音机来收听。

优点

  1. 观察者和被观察者之间是抽象耦合。非常容易扩展观察者和被观察者。
  2. 建立一套触发机制。根据单一职责原则,需要把每个单一职责类串联起来的时候。比如打猎打死了一只母鹿,母鹿三个幼崽饿死,尸体被两只秃鹰争抢,分配不均内斗,胜利的秃鹰生存下来并扩大了地盘。这就是一个触发机制,形成了一个触发链。

缺点

  1. 需要考虑开发效率和运行效率问题。一个被观察者、多个观察者,开发和调试会比较复杂。而且 java 中消息的通知默认是顺序执行,一个观察者卡壳,会影响整体的执行效率。此时一般采用异步的方式。
  2. 多级触发时更需要注意效率问题。

注意事项

  1. 广播链问题

    多级触发逻辑复杂,可维护性差。对于观察者来说还可能具有多重身份,既是观察者也是被观察者。经验建议在一个观察者模式中最多出现一个对象既是观察者又是被观察者,也就是消息最多转发一次(传递2次)。

  2. 异步处理问题

    被观察者发生动作,观察者需要做出回应,如果观察者比较多,处理时间长,需要用异步处理,此时就要考虑线程安全和队列的问题。可参考 Message Queue。

扩展

  1. java 世界中的观察者模式

    Java 中提供可扩展的父类 java.util.Observable,作为被观察者的父类。接口 java.util.Observer,作为观察者的父类。而子类只需实现自己的逻辑方法。

  2. 项目中真实的观察者模式,改造后包含以下3个方面:

    • 观察者和被观察者之间的消息沟通

      被观察者状态改变会触发观察者的一个行为,同时会传递一个消息给观察者。实际中的做法是:观察者中的 update 方法接受两个参数,一个是被观察者,一个是DTO数据传输对象,这个是纯洁的 javabean,由被观察者生成,由观察者消费。

      远程传输一般以 xml、json 等格式传递。

    • 观察者响应方式

      一个观察者多个被观察者的情况下,需要考虑性能问题。如果观察者来不及响应,被观察者的执行时间是不是也会被拉长?问题就在于观察者如何快速响应,有如下两个办法。

      办法一:采用多线程技术,不论被观察者启动线程还是观察者启动线程,都可以明显地提高系统性能。这是异步架构。

      办法二:采用缓存技术,保证足够的资源快速响应。代价是开发难度大,而且压测要足够充分。这是同步架构。

    • 被观察者尽量自己做主

      被观察者的状态改变不一定非要通知观察者。设计时要灵活考虑,避免加重观察者的逻辑处理。一般会对被观察者的业务逻辑 doSomething 方法重载,如增加一个 doSomething(boolean isNotifyObs) 方法,决定是否通知观察者,而不是在消息到达观察者时才判断是否要消费。

  3. 订阅发布模型

    EJB3 中有3个类型的Bean:Session Bean、EntityBean 和 MessageDriven Bean (消息驱动 Bean),消息的发布者(Provider)发布一个消息 MDB,通过 EJB 容器(一般是 Message Queue 消息队列)通知订阅者做出回应。

代码演示

以电脑为例,包含显示器、风扇、系统、键盘等组件。电源接通时,通知各个组件进行工作,各个组件也会监听键盘、鼠标进行响应。

1
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 观察者
class ScreenImpl implements Observer {
public void show() { System.out.println("屏幕开始显示!"); }
public void close() { System.out.println("屏幕关闭!"); }

@Override
public void update(Observable o, Object arg) {
String command = String.valueOf(arg);
if (command.equalsIgnoreCase("start")) {
show();
} else if (command.equalsIgnoreCase("shutdown")) {
close();
} else {
System.out.println("屏幕保持不变!");
}
}
}

class SystemImpl implements Observer {
public void work() { System.out.println("系统开始工作!"); }
public void shutdown() { System.out.println("系统关闭!"); }

@Override
public void update(Observable o, Object arg) {
String command = String.valueOf(arg);
if (command.equalsIgnoreCase("start")) {
work();
} else if (command.equalsIgnoreCase("shutdown")) {
shutdown();
} else {
System.out.println("系统不认识这个命令!");
}
}
}

// 被观察者
interface Keyboard {
void press(String command);
}

class KeyboardImpl extends Observable implements Keyboard {
public KeyboardImpl() {
super.addObserver(new ScreenImpl());
super.addObserver(new SystemImpl());
}

@Override
public void press(String command) {
System.out.println("按下一个命令:" + command);
super.setChanged();
super.notifyObservers(command);
}
}

// 高层模块
class Client {
public static void main(String[] args) {
Keyboard keyboard = new KeyboardImpl();
keyboard.press("start");
keyboard.press("xxxxx");
keyboard.press("shutdown");
}
}

备忘录模式 Memento

提供一种程序数据的备份方法。包含以下角色:

  1. Originator 发起人:记录当前时刻的内部状态,负责定义哪些属于备份范围的状态,负责创建和恢复备忘录数据。
  2. Memento 备忘录:存储 Originator 对象的内部状态,在需要的时候提供发起人需要的内部状态。
  3. Caretaker 备忘录管理员:管理、保存、提供备忘录。

类图

使用场景

  1. 需要保存和恢复数据的相关状态场景。
  2. 提供一个可回滚 rollback 的操作,比如 word 中的 CTRL+Z 组合键,浏览器中的后退按钮,文件管理器上的 backspace 键等。
  3. 需要监控的副本场景。例如要监控一个对象的属性,但监控不应该作为系统的主业务来调用,它只是边缘应用,即使出现监控不准、错误报警也影响不大,因此一般的做法是备份一个主线程中的对象,然后由分析程序来分析。
  4. 数据库连接如 JDBC 驱动的事务管理就是用的备忘录模式。

注意事项

  1. 备忘录的生命周期

    备忘录创建出来就要在 “最近” 的代码中使用,要主动管理它的生命周期,建立就要使用,不需要就要立刻删除其引用,等待 gc 回收。

  2. 备忘录的性能

    不要在频繁建立备份的场景中使用备忘录模式(比如一个 for 循环中),原因有二:1. 控制不了备忘录建立的对象数量;2. 打对象的建立比较消耗资源,需考虑系统性能。

扩展

  1. clone 方式的备忘录(代码演示)

    适用于较简单的场景,要避免与其他的对象产生严重的耦合关系。

    该方式把状态保留在了发起人内部,更方便。原因在于 java 的诞生比提出设计模式略晚。

  2. 多状态的备忘录模式(代码演示)

    对象不可能只有一个状态,一个 javabean 存在多个属性。对象全状态备份方案有多种处理方式,比如使用 clone、数据技术 DTO 回写临时表、使用 BeanUtils 类等。

  3. 多备份的备忘录(代码演示)

    实现多备份的检查点 Check Point 设计思路:修改备忘录的容器为 map 类型。注意内存一出问题,设计时要严格限定备忘录的创建,增加 map 的上限。

  4. 设置备份无法修改,保证备份数据纯洁(代码演示)

    使用内部类,设置为 private 。采用双接口设计:一个是业务的正常接口(宽接口),必要的业务逻辑;另一个是空接口(窄接口),什么方法都没有,目的是提供给子系统外的模块访问,比如容器对象,没有提供任何操纵数据的方法,相对比较安全。

代码演示

  1. 标准备忘录

    1
    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
    // 发起人
    @Data
    class Originator {
    // 内部状态
    private String state = "初始状态";
    // 创建备忘录
    public Memento createMemento() { return new Memento(this.state); }
    // 恢复备忘录
    public void restoreMemento(Memento memento) { this.setState(memento.getState()); }
    }

    // 备忘录
    @Data
    @AllArgsConstructor
    class Memento {
    // 发起人的内部状态
    private String state = "";
    }

    // 备忘录管理员
    @Data
    class Caretaker {
    private Memento memento;
    }

    class Client1 {
    public static void main(String[] args) {
    Originator originator = new Originator();
    Caretaker caretaker = new Caretaker();
    // 创建备忘录
    caretaker.setMemento(originator.createMemento());
    originator.setState("改变了状态");
    System.out.println("当前发起人的状态是:" + originator.getState());
    // 恢复备忘录
    originator.restoreMemento(caretaker.getMemento());
    System.out.println("恢复后发起人的状态是:" + originator.getState());
    }
    }
  2. clone 方式备忘录

    1
    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
    @Data
    class Originator2 implements Cloneable {
    private Originator2 backup;
    private String state = "初始状态";

    public void createMemento() throws CloneNotSupportedException {
    backup = this.clone();
    }

    public void restoreMemento(Originator2 originator2) throws NoSuchFieldException {
    if (backup != null) {
    this.setState(backup.getState());
    } else {
    throw new RuntimeException("备份为null");
    }
    }

    @Override
    protected Originator2 clone() throws CloneNotSupportedException {
    return (Originator2) super.clone();
    }
    }

    class Client2 {
    public static void main(String[] args) throws CloneNotSupportedException {
    Originator2 originator2 = new Originator2();
    originator2.createMemento();
    originator2.setState("修改了状态");
    System.out.println("修改后的状态: " + originator2.getState());
    originator2.setState(originator2.getBackup().getState());
    System.out.println("恢复后的状态: " + originator2.getState());
    }
    }
  3. 多状态备忘录

    1
    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    @Data
    class Originator3 {
    private String state1 = "初始状态111";
    private String state2 = "初始状态222";
    private String state3 = "初始状态333";

    public Memento3 createMemento() {
    return new Memento3(BeanUtils.backupProp(this));
    }
    public void restoreMemento(Memento3 memento3) {
    BeanUtils.restoreProp(this, memento3.getStateMap());
    }

    @Override
    public String toString() {
    final StringBuilder sb = new StringBuilder("Originator3{");
    sb.append("state1='").append(state1).append('\'');
    sb.append(", state2='").append(state2).append('\'');
    sb.append(", state3='").append(state3).append('\'');
    sb.append('}');
    return sb.toString();
    }
    }

    @Data
    @AllArgsConstructor
    class Memento3 {
    private Map<String, Object> stateMap;
    }

    // 工具类
    class BeanUtils {
    public static Map<String, Object> backupProp(Object source) {
    Map<String, Object> stateMap = new HashMap<>();
    try {
    // 获得 bean 信息
    BeanInfo beanInfo = Introspector.getBeanInfo(source.getClass());
    // 属性信息
    PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
    // 遍历所有属性
    for (PropertyDescriptor descriptor : descriptors) {
    // 属性名
    String fieldName = descriptor.getName();
    // getter方法
    Method getter = descriptor.getReadMethod();
    // 属性值
    Object fieldValue = getter.invoke(source);
    if (!"class".equalsIgnoreCase(fieldName)) {
    stateMap.put(fieldName, fieldValue);
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    return stateMap;
    }

    public static void restoreProp(Object target, Map<String, Object> propMap) {
    try {
    BeanInfo beanInfo = Introspector.getBeanInfo(target.getClass());
    PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
    for (PropertyDescriptor descriptor : descriptors) {
    String fieldName = descriptor.getName();
    if (propMap.containsKey(fieldName)) {
    Method setter = descriptor.getWriteMethod();
    setter.invoke(target, propMap.get(fieldName));
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

    class Client3 {
    public static void main(String[] args) {
    Originator3 originator3 = new Originator3();
    Memento3 memento3 = originator3.createMemento();
    originator3.setState1("变了1");
    originator3.setState2("变了2");
    originator3.setState3("变了3");
    System.out.println(originator3.toString());
    originator3.restoreMemento(memento3);
    System.out.println(originator3.toString());
    }
    }
  4. 多备份备忘录

    1
    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
    @Data
    class Originator4 {
    private String state = "初始状态";
    public Memento4 createMemento() {
    return new Memento4(this.state);
    }
    public void restoreMemento(Memento4 memento4) {
    this.state = memento4.getState();
    }
    }

    @Data
    @AllArgsConstructor
    class Memento4 {
    private String state;
    }

    class Caretaker4 {
    // 容纳多版本备忘录的容器
    private Map<String, Memento4> memMap = new HashMap<>();
    public Memento4 getMemento(String key) {
    return this.memMap.get(key);
    }
    public void setMemento(String key, Memento4 memento4) {
    this.memMap.put(key, memento4);
    }
    }

    class Client {
    public static void main(String[] args) {
    Originator4 originator4 = new Originator4();
    Caretaker4 caretaker4 = new Caretaker4();
    Memento4 memento4 = originator4.createMemento();
    memento4.setState("100状态");
    caretaker4.setMemento("100", memento4);
    caretaker4.setMemento("200", originator4.createMemento());
    originator4.setState("发起人拥有了新状态");
    System.out.println("此时发起人的状态:" + originator4.getState());
    originator4.restoreMemento(caretaker4.getMemento("100"));
    System.out.println("恢复100后的状态:" + originator4.getState());
    }
    }
  5. 双接口设计备忘录,防修改

    1
    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
    @Data
    class Originator5 {
    private String state = "初始状态";

    public IMemento createMemento() {
    return new Memento5(this.state);
    }
    public void restoreMemento(IMemento memento) {
    this.setState(memento.getState());
    }

    @Data
    @AllArgsConstructor
    private class Memento5 implements IMemento {
    private String state;
    }
    }

    // 窄接口,提供外部访问
    interface IMemento {
    String getState();
    }

    @Data
    class Caretaker5 {
    private IMemento iMemento;
    }

    class Client5 {
    public static void main(String[] args) {
    Originator5 originator5 = new Originator5();
    IMemento memento = originator5.createMemento();
    Caretaker5 caretaker5 = new Caretaker5();
    caretaker5.setIMemento(memento);

    originator5.setState("修改了状态");
    System.out.println("当前 originator 的状态:" + originator5.getState());

    originator5.restoreMemento(caretaker5.getIMemento());
    System.out.println("执行恢复后的状态:" + originator5.getState());
    }
    }

访问者模式 Visitor

将多种对象的操作交给访问者处理,避免对象和处理各自变化产生的耦合,在单一职责原则基础上实现良好的扩展性。主要包含以下角色:

  1. Visitor 抽象访问者:抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是 visit 方法的参数定义哪些对象可以被访问。
  2. ConcreteVisitor 具体访问者:访问者访问一个类后具体要做的事。
  3. Element 抽象元素:接口或者抽象类,声明接受哪一类访问者访问,程序中通过 accept 方法参数定义。
  4. ConcreteElement 具体元素:实现 accept 方法,通常是 visitor.visit(this),已称为固定写法。
  5. ObjectStructure 结构对象:元素产生者,一般容纳在多个不同类、不同接口的容器,如 List、Set、Map 等,在项目中,一般很少抽象出这个角色。

类图

使用场景

  1. 一个对象结果包含很多类对象,它们有不同的接口,需要对这些对象实施一些依赖于其具体类的操作,迭代器模式已经不能胜任的情况。

  2. 需要对一个对象结果中的对象进行多种不相关操作,同时想避免这些操作 “污染” 这些对象的类的情况。

  3. 总结来说就是,业务规则要求遍历多个不同的对象。

    迭代器模式只能访问同类或同接口的数据(当然用 instance of 可以访问所有数据),而访问者模式时对迭代器模式的扩充可以遍历不同的对象,针对不同的访问对象执行不同的操作。

  4. 访问者模式还可以用来充当拦截器(Interceptor)角色。

  5. 特别适用于大规模重构项目,集中规整。在一个阶段需求已经非常清晰,原系统功能点也已经明确,通过访问者模式可以很容易把一些功能进行梳理,达到最终目的——功能集中化,如一个统一的报表运算、UI展现等。还可以与其他模式混编建立一套自己的过滤器或者拦截器。(参考混编模式)

优点

  1. 符合单一职责原则

    具体元素角色 Employee(抽象类的两个子类)负责数据的加载,而 Visitor 类负责报表展现,两个不同的职责明确分离,各自变化。

  2. 优秀的扩展性

    职责分离,方便继续增加对数据的操作。直接在 Visitor 中增加一个方法,传递数据后进行处理即可。

  3. 灵活性非常高

    比如统计所有员工的工资,员工、部门经理、总经理都有各自的系数,通过遍历循环相加不是个好办法。这时可以采用访问者模式,将数据交给访问者计算。

缺点

  1. 具体元素对访问者公布细节,违背迪米特法则

    访问者要访问一个类就必然要这个类公布需要的方法和数据,这就是说访问者关注了其他类的内部细节。

  2. 具体元素变更比较困难

    具体元素角色的增加、删除、修改都比较难。比如要想增删一个成员变量,Visitor 就得做修改。

  3. 违背依赖倒置原则

    访问者依赖的是具体元素,而不是抽象元素。在面向对象的编程中,抛弃对接口的依赖,直接依赖实现类,扩展会比较难。

扩展

  1. 统计功能(代码演示)

    金融类企业常用汇总和报表功能,比如统计工资总额,不建议采用数据库存储过程,采用访问者模式,在 IVisitor 中增加 getTotalSalary 方法,实现类实现即可。

  2. 多个访问者(代码演示)

    比如展示表、汇总表,同一堆数据两种处理方式,从程序上看,一个类就有2个不同的访问者。

  3. 双分派(代码演示)

    一个演员可以扮演很多角色,系统要适应这种变化,也就是根据演员、角色两个对象类型,完成不同操作任务,这时就需要使用访问者模式。

    通过调用者实际类型+方法参数中的类型才能确定方法版本时,这种分派称作多分派。只依据调用者和方法参数,叫做单分派。

    双分派 double dispatch 意味着得到执行的操作由请求种类和两个接收者类型决定,属于多分派的一个特例。java 是一个支持双分派的单分派语言。

    延伸阅读:java方法调用之单分派与多分派

    java 静态、动态绑定依据重载 overload 和覆写 overide 实现。重载在编译器决定调用哪个方法,它根据表面类型决定方法调用,这是静态绑定;而执行该方法是由实际类型决定的,这是动态绑定。

    定义语言分派时一般指语言动态运行时的分派方式,java 在动态运行时就是根据调用者最终类型来区分要方法调用的方法的,所以说 java 是单分派语言。然而 java 在编译时是根据调用者本身类型和方法参数共同确定调用方法的,所以是静态多分派。

代码演示

  1. 统计功能

    以制作公司员工以及管理报表、计算工资总额为例。

    1
    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    interface IVisitor1 {
    void visit(CommonEmployee commonEmployee);
    void visit(Manager manager);
    int getTotalSalary();
    }

    class Visitor1 implements IVisitor1 {
    private int commonEmplayTotalSalary = 0;
    private int managerTotalSalary = 0;
    private final static int COMMONEMPLOYEE_COEFICIENT = 2;
    private final static int MANAGER_COEFICIENT = 5;

    @Override
    public int getTotalSalary() {
    return commonEmplayTotalSalary + managerTotalSalary;
    }
    private int getCommonEmployeeTotalSalary(int salary) {
    return commonEmplayTotalSalary + salary * COMMONEMPLOYEE_COEFICIENT;
    }
    private int getManagerTotalSalary(int salary) {
    return managerTotalSalary + salary * MANAGER_COEFICIENT;
    }

    @Override
    public void visit(CommonEmployee commonEmployee) {
    System.out.println(getCommonEmployeeInfo(commonEmployee));
    this.commonEmplayTotalSalary = getCommonEmployeeTotalSalary(commonEmployee.getSalary());
    }
    @Override
    public void visit(Manager manager) {
    System.out.println(getManagerInfo(manager));
    this.managerTotalSalary = getManagerTotalSalary(manager.getSalary());
    }
    private String getCommonEmployeeInfo(CommonEmployee commonEmployee) {
    return getBasicInfo(commonEmployee) + "job=" + commonEmployee.getJob();
    }
    private String getManagerInfo(Manager manager) {
    return getBasicInfo(manager) + "performance=" + manager.getPerformance();
    }

    private String getBasicInfo(Employee employee) {
    return new StringJoiner("\t", "", "\t\t")
    .add("name=" + employee.getName())
    .add("salary=" + employee.getSalary())
    .toString();
    }
    }

    @Data
    abstract class Employee {
    private String name;
    private int salary;
    public abstract void accept(IVisitor1 visitor);
    }

    @Data
    class CommonEmployee extends Employee {
    private String job;
    @Override
    public void accept(IVisitor1 visitor) { visitor.visit(this); }
    }

    @Data
    class Manager extends Employee {
    private String performance;
    @Override
    public void accept(IVisitor1 visitor) { visitor.visit(this); }
    }

    class Client1 {
    public static void main(String[] args) {
    Visitor1 visitor = new Visitor1();
    for (Employee employee : mockEmployee()) {
    employee.accept(visitor);
    }
    System.out.println("\n工资总额:" + visitor.getTotalSalary());
    }

    private static List<Employee> mockEmployee() {
    List<Employee> employeeList = new ArrayList<>();

    CommonEmployee zhangsan = new CommonEmployee();
    zhangsan.setName("张三");
    zhangsan.setSalary(1800);
    zhangsan.setJob("程序猿");

    CommonEmployee lisi = new CommonEmployee();
    lisi.setName("李四");
    lisi.setSalary(2000);
    lisi.setJob("产品汪");

    Manager manager = new Manager();
    manager.setName("主管");
    manager.setSalary(5000);
    manager.setPerformance("马马虎虎");

    employeeList.add(zhangsan);
    employeeList.add(lisi);
    employeeList.add(manager);

    return employeeList;
    }
    }
  2. 多个访问者

    上述例子中,把展示表、汇总表分拆为两个 visitor 接口。

    1
    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    interface IVisitor2 {
    void visit(CommonEmployee2 commonEmployee2);
    void visit(Manager2 manager2);
    }

    // 元素类,决定哪些访问者可以访问
    @Data
    abstract class Employee2 {
    private String name;
    private int salary;
    public abstract void accept(IVisitor2 iVisitor2);
    }

    @Data
    class CommonEmployee2 extends Employee2 {
    private String job;
    @Override
    public void accept(IVisitor2 iVisitor2) {
    iVisitor2.visit(this);
    }
    }

    @Data
    class Manager2 extends Employee2 {
    private String performance;
    @Override
    public void accept(IVisitor2 iVisitor2) {
    iVisitor2.visit(this);
    }
    }

    // 展示表访问者
    interface IShowVisitor2 extends IVisitor2 {
    void report();
    }
    // 工资统计表访问者
    interface IStatisticsVisitor2 extends IVisitor2 {
    void statisticsSalary();
    }

    class ShowVisitor2 implements IShowVisitor2 {
    private Map<String, String> infoMap = new HashMap<>();

    @Override
    public void visit(CommonEmployee2 commonEmployee2) {
    infoMap.put(commonEmployee2.getName(), getCommonEmployeeInfo(commonEmployee2));
    }
    @Override
    public void visit(Manager2 manager2) {
    infoMap.put(manager2.getName(), getManagerInfo(manager2));
    }

    @Override
    public void report() {
    for (Map.Entry<String, String> entry : infoMap.entrySet()) {
    System.out.println(entry.getValue());
    }
    }

    private String getCommonEmployeeInfo(CommonEmployee2 commonEmployee2) {
    return getBasicInfo(commonEmployee2) + "job=" + commonEmployee2.getJob();
    }
    private String getManagerInfo(Manager2 manager2) {
    return getBasicInfo(manager2) + "performance=" + manager2.getPerformance();
    }
    private String getBasicInfo(Employee2 employee2) {
    return new StringJoiner("\t", "", "\t\t")
    .add("name=" + employee2.getName())
    .add("salary=" + employee2.getSalary())
    .toString();
    }
    }

    class StatisticsVisitor2 implements IStatisticsVisitor2 {
    private int commonEmplayTotalSalary = 0;
    private int managerTotalSalary = 0;
    private final static int COMMONEMPLOYEE_COEFICIENT = 2;
    private final static int MANAGER_COEFICIENT = 5;

    @Override
    public void visit(CommonEmployee2 commonEmployee2) {
    this.commonEmplayTotalSalary += commonEmployee2.getSalary() * COMMONEMPLOYEE_COEFICIENT;
    }
    @Override
    public void visit(Manager2 manager2) {
    this.managerTotalSalary += manager2.getSalary() * MANAGER_COEFICIENT;
    }

    @Override
    public void statisticsSalary() {
    System.out.println("工资总额:" + (this.commonEmplayTotalSalary + this.managerTotalSalary));
    }
    }

    class Client2 {
    public static void main(String[] args) {
    IShowVisitor2 showVisitor2 = new ShowVisitor2();
    IStatisticsVisitor2 statisticsVisitor2 = new StatisticsVisitor2();
    for (Employee2 employee : mockEmployee()) {
    employee.accept(showVisitor2);
    employee.accept(statisticsVisitor2);
    }
    showVisitor2.report();
    statisticsVisitor2.statisticsSalary();
    }

    private static List<Employee2> mockEmployee() {
    List<Employee2> employeeList = new ArrayList<>();

    CommonEmployee2 zhangsan = new CommonEmployee2();
    zhangsan.setName("张三");
    zhangsan.setSalary(1800);
    zhangsan.setJob("程序猿");

    CommonEmployee2 lisi = new CommonEmployee2();
    lisi.setName("李四");
    lisi.setSalary(2000);
    lisi.setJob("产品汪");

    Manager2 manager = new Manager2();
    manager.setName("主管");
    manager.setSalary(5000);
    manager.setPerformance("绩效很高");

    employeeList.add(zhangsan);
    employeeList.add(lisi);
    employeeList.add(manager);

    return employeeList;
    }
    }
  3. 双分派

    以演员演电影为例,一个演员可以演多个角色,系统要根据演员、角色两个类型共同决定完成什么任务。
    通过访问者模式解决演员、角色变化的问题,由请求种类和两个接收者类型决定怎么执行,实现双分派。

    1
    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
    interface Role {}
    class KungFuRole implements Role {}
    class ComedyRole implements Role {}

    abstract class AbstractActor {
    public void act(Role role) { System.out.println("演员可以扮演任何角色"); }
    // 重载
    public void act(KungFuRole kungFuRole) { System.out.println("演员可以演功夫角色"); }
    }

    class Actor1 extends AbstractActor {
    // 重写
    @Override
    public void act(KungFuRole kungFuRole) { System.out.println("这个类的演员都会功夫,拍打片非常合适!"); }
    }

    class Actor2 extends AbstractActor {
    @Override
    public void act(KungFuRole kungFuRole) { System.out.println("这个类型的演员不太擅长演功夫角色,需要指导"); }
    }

    class Client4 {
    public static void main(String[] args) {
    Role role = new KungFuRole();
    AbstractActor actor = new Actor2();
    // 执行一下两个方法就会发现,重载在编译期就决定了要调用哪个方法,虽然 actor.act(role) 用的子类构造。
    // 根据 role 的表面类型调用 act(Role role) 方法,这是静态绑定。而 Actor 的执行方法 act 则是由实际类型决定的,这是动态绑定。
    actor.act(role);
    actor.act(new KungFuRole());
    }
    }

    修改后:

    1
    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
    interface RoleTwo {
    void accept(AbstractActorTwo actor);
    }

    class KungFuRoleTwo implements RoleTwo {
    @Override
    public void accept(AbstractActorTwo actor) { actor.act(this); }
    }

    class ComedyRoleTwo implements RoleTwo {
    @Override
    public void accept(AbstractActorTwo actor) { actor.act(this); }
    }

    abstract class AbstractActorTwo {
    public void act(RoleTwo role) { System.out.println("演员可以扮演任何角色"); }
    public void act(KungFuRoleTwo kungFuRole) { System.out.println("演员可以演功夫角色"); }
    }

    class ActorTwo1 extends AbstractActorTwo {
    @Override
    public void act(KungFuRoleTwo kungFuRole) { System.out.println("这个类的演员都会功夫,拍打片非常合适!"); }
    }

    class ActorTwo2 extends AbstractActorTwo {
    @Override
    public void act(KungFuRoleTwo kungFuRole) { System.out.println("这个类型的演员不太擅长演功夫角色,需要指导"); }
    }

    class ClientTwo {
    public static void main(String[] args) {
    RoleTwo roleTwo = new KungFuRoleTwo();
    AbstractActorTwo actorTwo = new ActorTwo2();
    roleTwo.accept(actorTwo);
    roleTwo.accept(new ActorTwo2());
    }
    }

状态模式 State

核心是封装,状态变更引起行为变更,从外部看起来就像这个对象对应的类发生了改变,包含三个角色:

  1. State 抽象状态角色:接口或抽象类,负责对象状态定义,并且封装环境角色以实现状态切换。
  2. ConcreteState 具体状态角色:每一个具体状态必须完成2个职责,本状态的行为管理和趋向状态处理,通俗而言就是本状态下要做的事以及本状态如何过度到其他状态。
  3. Context 环境角色:定义客户需要的接口,并负责具体状态切换。

类图

使用场景

  1. 行为随状态改变而改变的场景

    这是状态模式的根本出发点,例如权限设计,人员的状态不同即使执行相同的行为结果也可能不同。

  2. 条件、分支判断语句的替代者

    避免使用 switch、if 的地方可以考虑,通过扩展子类实现条件的判断处理。

优点

  1. 结构清晰

    避免过多 switch..case… 或 if…else… 语句的使用,避免程序复杂性,提升可维护性。

  2. 遵循开闭原则和单一职责原则

    每一个状态都是一个子类,想增加或修改状态只需要增加或修改子类即可。

  3. 封装性非常好

    状态变换放到类内部实现,外部的调用不需要知道类内如何实现状态和行为的变换。

缺点

  1. 子类可能会膨胀

    可在数据库中建立一个状态表,然后根据状态执行相应的操作。

注意事项

  1. 行为受状态约束时可使用状态模式,对象的状态最好不要超过5个。

扩展

  1. 状态模式+建造者模式

    状态自由切换种类太多,可使用建造者模式把已有的几种状态按照一定顺序进行封装。

  2. 状态机 State Machine

    Context 类的升级版,工作流开发中常用。

代码演示

1
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
49
50
51
52
53
54
abstract class State {
@Setter
protected Context context;
abstract void handleState1();
abstract void handleState2();
}

class ConcreteState1 extends State {
@Override
void handleState1() {
System.out.println("当前状态:" + super.context.getCurrentState() + ",开始执行状态1的动作");
}
@Override
void handleState2() {
super.context.setCurrentState(Context.STATE2);
super.context.handleState2();
}
}

class ConcreteState2 extends State {
@Override
void handleState1() {
super.context.setCurrentState(Context.STATE1);
super.context.handleState1();
}
@Override
void handleState2() {
System.out.println("当前状态:" + super.context.getCurrentState()+ ",开始执行状态2的动作");
}
}

class Context {
public final static State STATE1 = new ConcreteState1();
public final static State STATE2 = new ConcreteState2();
@Getter
private State currentState;

public void setCurrentState(State currentState) {
this.currentState = currentState;
this.currentState.setContext(this);
}

public void handleState1() { this.currentState.handleState1(); }
public void handleState2() { this.currentState.handleState2(); }
}

class Client {
public static void main(String[] args) {
Context context = new Context();
context.setCurrentState(Context.STATE1);
context.handleState2();
context.handleState1();
}
}

解释器模式 Interpreter

按照规定语法进行解析的模式。这个模式主要理解概念和理念,实际应用较少。以公式 a+b+c-d 为例,先介绍几个基本概念:

  1. 运算元素:也叫做终结符号。指 a、b、c 等符号,需要具体赋值的对象。这些元素除了需要赋值外,不需要做任何处理,所有运算元素都对应一个具体的业务参数,是语法中最小的单元逻辑,不可再拆分。
  2. 运算符号:也叫非终结符号。指 +、- 等符号,需要编写算法进行处理,每个运算符号对咬对应处理单元,否则公式无法运行。

两类元素的共同点是都要被解析,不同点是所有的运算元素具有相同的功能,可以用一个类表示,而运算符号则需要分别进行解释,例如加法需要加法解析器,减法需要减法解析器。

解析器模式包含以下4个角色:

  1. AbstractExpression 抽象解释器:具体的解释任务由各实现类完成,具体解释器分别由 TerminalExpression 和 NonterminalExpression 完成。
  2. TerminalExpression 终结符表达式:实现与文法中的元素相关联的解释操作,通常一个解释器模式中只有一个终结符表达式,但有多个实例,对应不同的终结符。
  3. NonterminalExpression 非终结符表达式:文法中的每条规则对应于一个非终结表达式,根据逻辑的复杂程度而增加,原则上每个文法规则都对应一个非终结符表达式。
  4. Context 环境角色:例如用 HashMap 代替。

类图

使用场景

  1. 重复发生的问题

    例如多个应用服务器每天产生大量日志,需要对日志文件进行分析处理,各服务器的日志格式不同但数据要素相同,按照解释器的说法就是终结符表达式都是相同的,但是非终结符表达式则需要制定。

  2. 一个简单语法需要解释的场景

    对于非终结表达式,文法规则越多,复杂度越高,而且类间还要进行递归调用,需要极大耐心和信心去排查问题。因此解释器一般用来解析比较标准的字符集,例如 SQL 语法分析,不过该部分逐渐被专用工具代替。

  3. 某些特定的商业环境

    现在模型运算的例子很多,目前很多商业机构已经能够提供出大量的数据进行分析。

优点

  1. 扩展性良好

    解释器是一个简单语法分析工具,修改语法规则只要修改相应的非终结符表达式就可以了,若扩展语法,则只需要增加非终结符类就可以。

缺点

  1. 会引起类膨胀

    每个语法都要产生一个非终结符表达式,语法规则比较复杂时,可能产生大量的类文件,为维护带来非常多得麻烦。

  2. 采用递归调用方法

    每个非终结符表达式只关心与自己有关的表达式,每个表达式需要知道最终的结果,必须一层层剥茧,不论面向过程还是面向对象的语言,递归都是在必要条件下使用的,导致调试非常复杂。想想看,如果要排查一个语法错误,还需要一个个断点调试下去,知道最小的语法单元。

  3. 效率问题

    使用了大量的循环和递归。在用于解析复杂、冗长的语法时,效率是难以忍受的。

注意事项

  1. 尽量不要在重要的模块中使用解释器模式,否则维护会是一个大问题。在项目中可以使用 shell、JRuby、Groovy 等脚本语言来代替解释器模式,弥补 java 编译型语言的不足。比如在银行分析型项目中采用 JRuby 进行运算处理,避免使用解释器模式的四则匀速,保证效率和性能。
  2. 解释器模式在实际的系统开发中使用较少,一般在大中型的框架型项目能够找到它的身影,如一些数据分析工具、报表设计工具、科学计算工具等。若确实遇到 “一种特定类型的问题发生的频率足够高” 的情况,准备使用解释器模式时,可以考虑 Expression4J、MESP(Math Expression String Parser)、Jep 等开源的解析工具包,尽量避免自己从头编写解释器。

代码演示

  1. 通用源码

    1
    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    /**
    * 环境角色,例如存放 hashmap
    */
    class Context {}

    /**
    * 抽象表达式是生成语法集合(也叫语法树)的关键,每个语法集合完成指定语法解析任务,通过递归调用,最终由最小的语法单元进行解析完成
    */
    abstract class Expression {
    public abstract Object interpreter(Context ctx);
    }

    /**
    * 终结符表达式主要处理场景元素和数据的转换。终结符表达式不能再参与比自己更小的文法运算。
    */
    class TerminalExpression extends Expression {
    // 通常终结符表达式只有一个,但是有多个对象
    @Override
    public Object interpreter(Context ctx) {
    return null;
    }
    }

    /**
    * 每个非终结符表达式都代表了一个文法规则,并且每个文法规则都只关心自己周边的文法规则的结果,
    * 因此产生了每个非终结表达式调用自己周边的非终结符表达式,然后最终、最小的文法规则就是终结符表达式。
    *
    */
    class NonterminalExpression extends Expression {
    // 每个非终结符表达式都会对其他表达式产生依赖

    public NonterminalExpression(Expression... expressions) {
    }

    @Override
    public Object interpreter(Context ctx) {
    return null;
    }
    }

    /**
    * 封装类,传递进来一个规范语法文件,解析器分析后产生结果并返回,避免了调用者与语法解析器的耦合关系
    */
    class Client {
    public static void main(String[] args) {
    Context context = new Context();
    // 通常定义一个语法容器,容纳一个具体的表达式,例如 ListArray、LinkedList、Stack 等类型
    Stack<Expression> stack = null;
    for (; ; ) {
    // 进行语法判断,并产生递归调用
    break;
    }
    // 产生一个完整的语法树,由多个具体的语法分析进行解析
    Expression exp = stack.pop();
    exp.interpreter(context);
    }
    }
  2. 案例

    输入一个模型公式(例如加减运算),然后输入模型中的参数,运算出结果。

    1
    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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    // 抽象表达式
    abstract class AbstractExpression {
    // 解析公式和数值,其中 key 是公式中的参数,value 是具体的数字
    abstract int interpreter(HashMap<String, Integer> var);
    }

    // 变量解析器
    @AllArgsConstructor
    class VarExpression extends AbstractExpression {
    private String key;
    @Override
    int interpreter(HashMap<String, Integer> var) {
    return var.get(this.key);
    }
    }

    // 抽象运算符号解析器
    @AllArgsConstructor
    abstract class SymbolExpression extends AbstractExpression {
    // 所有的解析公式都应只关心自己左右两个表达式的结果
    protected AbstractExpression left;
    protected AbstractExpression right;
    }

    // 加法解析器
    class AddExpression extends SymbolExpression {
    public AddExpression(AbstractExpression left, AbstractExpression right) {
    super(left, right);
    }
    //把左右两个表达式运算的结果相加
    @Override
    int interpreter(HashMap<String, Integer> var) {
    return super.left.interpreter(var) + super.right.interpreter(var);
    }
    }

    // 减法解析器
    class SubExpression extends SymbolExpression {
    public SubExpression(AbstractExpression left, AbstractExpression right) {
    super(left, right);
    }
    @Override
    int interpreter(HashMap<String, Integer> var) {
    return super.left.interpreter(var) - super.right.interpreter(var);
    }
    }

    // 解析器封装类
    class Calculator {
    // 定义表达式
    private AbstractExpression expression;
    // 构造函数传参,并解析
    public Calculator(String expStr) {
    // 定义一个栈,安排运算的先后顺序
    Stack<AbstractExpression> stack = new Stack<>();
    // 表达式拆分为字符数组
    char[] charArray = expStr.toCharArray();
    // 运算
    AbstractExpression left = null;
    AbstractExpression right = null;
    for (int i = 0; i < charArray.length; i++) {
    switch (charArray[i]) {
    case '+':
    left = stack.pop();
    right = new VarExpression(String.valueOf(charArray[++i]));
    stack.push(new AddExpression(left, right));
    break;
    case '-':
    left = stack.pop();
    right = new VarExpression(String.valueOf(charArray[++i]));
    stack.push(new SubExpression(left, right));
    break;
    default:
    stack.push(new VarExpression(String.valueOf(charArray[i])));
    }
    }
    this.expression = stack.pop();
    }
    // 开始运算
    public int run(HashMap<String, Integer> var) {
    return this.expression.interpreter(var);
    }
    }

    // 客户模拟类
    class ClientCase {
    public static void main(String[] args) throws IOException {
    // 输入表达式
    String expStr = getExpStr();
    // 获得值映射
    HashMap<String, Integer> var = getValue(expStr);
    Calculator calculator = new Calculator(expStr);
    int result = calculator.run(var);
    System.out.println("执行结果是:" + result);
    }

    private static String getExpStr() throws IOException {
    System.out.println("请输入表达式:");
    InputStream in = System.in;
    InputStreamReader inputStreamReader = new InputStreamReader(in);
    BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
    return bufferedReader.readLine();
    }

    private static HashMap<String, Integer> getValue(String expStr) throws IOException {
    HashMap<String, Integer> map = new HashMap<>();
    // 解析有几个参数要传递
    for (char c : expStr.toCharArray()) {
    if (c != '+' && c != '-') {
    // 解决重复参数的问题
    if (!map.containsKey(String.valueOf(c))) {
    System.out.println("请输入" + c + "的值:");
    String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
    map.put(String.valueOf(c), Integer.valueOf(in));
    }
    }
    }
    return map;
    }
    }

 Comments