声明:文章内容绝大多数来自于南京大学 计算机科学与技术系 面向对象设计方法 课程课件

设计模式-行为型模式

  • 行为型模式(Behavioral Pattern) 关注系统中对象之间的交互,研究系统在运行时对象之间的相互通信与协作,进一步明确对象的职责
  • 行为型模式:不仅仅关注类和对象本身,还重点关注它们之间的相互作用和职责划分
  • 类行为型模式
    • 使用继承关系在几个类之间分配行为,主要通过多态等方式来分配父类与子类的职责
  • 对象行为型模式
    • 使用对象的关联关系来分配行为,主要通过对象关联等方式来分配两个或多个类的职责

image-20220612114952212

Chain of Responsibility Pattern 职责链模式

职责链模式:避免将一个请求的发送者与接受者耦合在一起,让多个对象都有机会处理请求。将处理请求的对象连接成一条链,并且沿着这条链传递请求,直到有一个对象能够处理它为止。

  • 职责链模式的定义
    • 请求的处理者组织成一条链,并让请求沿着链传递,由链上的处理者对请求进行相应的处理
    • 客户端无须关心请求的处理细节以及请求的传递,只需将请求发送到链上,将请求的发送者和请求的处理者解耦

image-20220612145032840

Handler:抽象处理者
ConcreteHandler:具体处理者

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
public class ChainOfResponsibilityPattern {
public static void main(String[] args) {
//组装责任链
Handler handler1 = new ConcreteHandler1();
Handler handler2 = new ConcreteHandler2();
handler1.setNext(handler2);
//提交请求
handler1.handleRequest("two");
}
}
//抽象处理者角色
abstract class Handler {
private Handler next;
public void setNext(Handler next) {
this.next = next;
}
public Handler getNext() {
return next;
}
//处理请求的方法
public abstract void handleRequest(String request);
}
//具体处理者角色1
class ConcreteHandler1 extends Handler {
public void handleRequest(String request) {
if (request.equals("one")) {
System.out.println("具体处理者1负责处理该请求!");
} else {
if (getNext() != null) {
getNext().handleRequest(request);
} else {
System.out.println("没有人处理该请求!");
}
}
}
}
//具体处理者角色2
class ConcreteHandler2 extends Handler {
public void handleRequest(String request) {
if (request.equals("two")) {
System.out.println("具体处理者2负责处理该请求!");
} else {
if (getNext() != null) {
getNext().handleRequest(request);
} else {
System.out.println("没有人处理该请求!");
}
}
}
}
  • 纯的职责链模式

    • 一个具体处理者对象只能在两个行为中选择一个:要么承担全部责任,要么将责任推给下家
    • 不允许出现某一个具体处理者对象在承担了一部分或全部责任后又将责任向下传递的情况
    • 一个请求必须被某一个处理者对象所接收,不能出现某个请求未被任何一个处理者对象处理的情况
  • 不纯的职责链模式

    • 允许某个请求被一个具体处理者部分处理后向下传递,或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求
    • 一个请求可以最终不被任何处理者对象所接收并处理

模式优点

  • 使得一个对象无须知道是其他哪一个对象处理其请求,降低了系统的耦合度
  • 可简化对象之间的相互连接
  • 给对象职责的分配带来更多的灵活性
  • 增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可

模式缺点

  • 不能保证请求一定会被处理
  • 对于比较长的职责链,系统性能将受到一定影响,在进行代码调试时不太方便
  • 如果建链不当,可能会造成循环调用,将导致系统陷入死循环

模式适用环境

  • 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定
  • 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求
  • 动态指定一组对象处理请求

Command Pattern 命令模式

  • 软件开发
    • 按钮 ——请求发送者
    • 时间处理类——请求的最终接受者和处理者
    • 发送者与接受者之间引入了新的命令对象(类似电线),将发送者的请求封装在命令对象中,再通过命令对象来调用接收者的方法。
    • 相同的按钮可以对应不同的事件处理类
  • 动机
    • 将请求发送者和接收者完全解耦
    • 发送者与接收者之间没有直接引用关系
    • 发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求

命令模式:将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化对请求排队或者记录请求日志,以及支持可撤销的操作。

  • 别名为动作(Action)模式或事务(Transaction)模式

image-20220612165502062

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
public class Receiver {
public void action(){
System.out.println("Action has been taken.");
}
}
public class Invoker {
private Command command;

public Invoker(Command command){
this.command = command;
}

public void action(){
command.execute();
}
}
public interface Command {
void execute();
}
public class ConcreteCommand implements Command {
private Receiver receiver;

public ConcreteCommand(Receiver receiver){
this.receiver = receiver;
}

public void execute() {
receiver.action();
}

}
public class CommandClient {

/**
* @param args
*/
public static void main(String[] args) {
Receiver receiver = new Receiver();
Command cmd = new ConcreteCommand(receiver);
Invoker invoker = new Invoker(cmd);
invoker.action();
}

}
  • Command 抽象命令类
  • ConcreteCommand 具体命令类
  • Invoker 调用类
  • Receiver 接收者
  • 命令模式的实现
    • 命令模式的本质是对请求进行封装
    • 一个请求对应于一个命令,将发出命令的责任和执行命令的责任分开
    • 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的

实现命令队列

以下代码实现来自设计模式解密(11)- 命令模式 - 扩展篇(请求日志) - Json_wangqiang - 博客园 (cnblogs.com)

  • 当一个请求发送者发送一个请求时,有不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理
  • 增加一个CommandQueue类,由该类负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者
  • 批处理
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
public class CommandQueue {    
//定义一个ArrayList来存储命令队列
private ArrayList<Command> commands = new ArrayList<Command>();

public void addCommand(Command command) {
commands.add(command);
}

public void removeCommand(Command command) {
commands.remove(command);
}

//循环调用每一个命令对象的execute()方法
public void execute() {
for (Object command : commands) {
((Command)command).execute();
}
}
}
public class Invoker {
private CommandQueue commandQueue; //维持一个CommandQueue对象的引用

//构造注入
public Invoker(CommandQueue commandQueue) {
this. commandQueue = commandQueue;
}

//设值注入
public void setCommandQueue(CommandQueue commandQueue) {
this.commandQueue = commandQueue;
}

//调用CommandQueue类的execute()方法
public void call() {
commandQueue.execute();
}
}

记录请求日志

以下代码实现来自设计模式解密(11)- 命令模式 - 扩展篇(请求日志) - Json_wangqiang - 博客园 (cnblogs.com)

  • 将请求的历史记录保存下来,通常**以日志文件(LogFile)**的形式永久存储在计算机中
    • 为系统提供一种恢复机制
    • 可以用于实现批处理
    • 防止因为断点或者系统重启等原因造成请求丢失,而且可以避免重新发送全部请求时造成某些命令的重复执行
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
132
133
134
135
136
137
/**
* 抽象命令类,由于需要将命令对象写入文件,因此它实现了Serializable接口
* @author Json
*/
public abstract class Command implements Serializable {
private static final long serialVersionUID = 1L;

protected String name; //命令名称
protected String args; //命令参数
protected Operator operator; //接收者对象

public Command(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public void setOperator(Operator operator) {
this.operator = operator;
}

//声明两个抽象的执行方法execute()
public abstract void execute(String args);
public abstract void execute();
}
/**
* 请求接收者 -- 由于Operator类的对象是Command的成员对象,它也将随Command对象一起写入文件,因此Operator也需要实现Serializable接口
* @author Json
*/
public class Operator implements Serializable{
private static final long serialVersionUID = 1L;

public void insert(String args) {
System.out.println("新增数据:" + args);
}

public void update(String args) {
System.out.println("修改数据:" + args);
}

public void delete(String args) {
System.out.println("删除数据:" + args);
}
}
/**
* 具体命令角色类 -- 插入命令
* @author Json
*/
public class InsertCommand extends Command{
public InsertCommand(String name) {
super(name);
}

public void execute(String args) {
this.args = args;
operator.insert(args);
}

public void execute() {
operator.insert(this.args);
}
}
/**
* 具体命令角色类 -- 修改命令
* @author Json
*/
public class UpdateCommand extends Command{
public UpdateCommand(String name) {
super(name);
}

public void execute(String args) {
this.args = args;
operator.update(args);
}

public void execute() {
operator.update(this.args);
}
}
/**
* 具体命令角色类 -- 删除命令
* @author Json
*/
public class DeleteCommand extends Command{
public DeleteCommand(String name) {
super(name);
}

public void execute(String args) {
this.args = args;
operator.delete(args);
}

public void execute() {
operator.delete(this.args);
}
}
/**
* 请求发送者
* @author Json
*/
public class SqlExecuteTool {
//定义一个集合来存储每一次操作时的命令对象
private ArrayList<Command> commands = new ArrayList<Command>();
private Command command;

//注入具体命令对象
public void setCommand(Command command) {
this.command = command;
}

//执行配置文件修改命令,同时将命令对象添加到命令集合中
public void call(String args) {
command.execute(args);
commands.add(command);
}

//记录请求日志,生成日志文件,将命令集合写入日志文件
public void save() {
FileUtil.writeCommands(commands);
}

//从日志文件中提取命令集合,并循环调用每一个命令对象的execute()方法来实现配置文件的重新设置
public void recover() {
ArrayList list = FileUtil.readCommands();
for (Object obj : list) {
((Command)obj).execute();
}
}
}

实现恢复操作

可以通过对命令类进行修改使得系统支持撤销(Undo)操作和恢复(Redo)操作

核心思想:使用逆向操作实现(通过增加execute对应的undo方法)

​ 或者使用备忘录模式直接恢复

以下示例为逆向操作实现范例

以下实现代码来自设计模式解密(11)- 命令模式 - 扩展篇(撤销命令) - Json_wangqiang - 博客园 (cnblogs.com)

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
/**
* 包含撤销命令的接口
* @author Json
*/
public interface Command {
//执行方法
void execute();
//撤销方法
void undo();
}
/**
* 具体命令 -- 创建目录
* @author Json
*/
public class CreateDirCommand implements Command {
private String dirName;
MakeDir makeDir;
public CreateDirCommand(MakeDir makeDir,String dirName) {
this.makeDir = makeDir;
this.dirName = dirName;
}

@Override
public void execute() {
makeDir.createDir(dirName);
}

@Override
public void undo() {
makeDir.deleteDir(dirName);
}
}
/**
* 命令接受者
* 包含两个命令:创建目录、删除目录
* @author Json
*/
public class MakeDir {
//创建目录
public void createDir(String name){
File dir = new File(name);
if (dir.exists()) {
System.out.println("创建目录 " + name + " 失败,目标目录已经存在");
}else{
//创建目录
if (dir.mkdirs()) {
System.out.println("创建目录 " + name + " 成功");
} else {
System.out.println("创建目录 " + name + " 失败");
}
}
}
//删除目录
public void deleteDir(String name){
File dir = new File(name);
if(dir.exists()) {
if(dir.delete()){
System.out.println("删除目录 " + name + " 成功");
}else{
System.out.println("删除目录 " + name + " 失败");
}
}else{
System.out.println("删除目录 " + name + " 失败,目标目录不存在");
}
}
}
/**
* 请求者
* @author Json
*/
public class RequestMakeDir {
Command createCommand;
public void setCreateCommand(Command command) {
this.createCommand = command;
}

public void executeCreateCommand(){
createCommand.execute();
}

public void undoCreateCommand(){
createCommand.undo();
}
}

模式优点

  • 降低系统的耦合度
  • 新的命令可以很容易地加入到系统中,符合开闭原则
  • 可以比较容易地设计一个命令队列或宏命令(组合命令)
  • 为请求的**撤销(Undo)和恢复(Redo)**操作提供了一种设计和实现方案

模式缺点

使用命令模式可能会导致某些系统有过多的具体命令类(针对每一个对请求接收者的调用操作都需要设计一个具体命令类)

模式适用环境

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
  • 系统需要在不同的时间指定请求、将请求排队和执行请求
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作
  • 系统需要将一组操作组合在一起形成宏命令

Interpreter Pattern 解释器模式

解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

  • 解释器模式的定义
    • 在解释器模式的定义中所指的“语言”是使用规定格式和语法的代码
    • 是一种使用频率相对较低但学习难度相对较大的设计模式,用于描述如何使用面向对象语言构成一个简单的语言解释器
    • 能够加深对面向对象思想的理解,并且理解编程语言中文法规则的解释过程
  • 解释器模式的结构

image-20220613202812736

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
public abstract class AbstractExpression {
public abstract void interpret(Context ctx);
}
public class TerminalExpression extends AbstractExpression {
public void interpret(Context ctx) {
//终结符表达式的解释操作
}
}
public class NonterminalExpression extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public NonterminalExpression(AbstractExpression
left,AbstractExpression right) {
this.left=left;
this.right=right;
}
public void interpret(Context ctx) {
//递归调用每一个组成部分的interpret()方法
//在递归调用时指定组成部分的连接方式,即非终结符的功能
}
}
public class Context {
private HashMap<String, String> map = new HashMap<String,
String>();
public void assign(String key, String value) {
//往环境类中设值
map.put(key, value);
}
public String lookup(String key) {
//获取存储在环境类中的值
return map.get(key);
}
}

模式优点

  • 易于改变和扩展文法:由于在解释器模式中使用类表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
  • 可以方便地实现一个简单的语言:每一条文法规则都可以表示为一个类。
  • 实现文法较为容易(有自动生成工具)。
  • 增加新的解释表达式较为方便,符合开闭原则。

模式缺点

  • 对于复杂文法难以维护:在解释器中每一条规则至少需要定义一个类,因此,一个语言如果包含太多文法规则,类的个数会急剧增加,导致系统难以管理和维护,此时可以考虑语法分析程序等方式来取代解释器模式。
  • 执行效率较低:由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调用过程也比较麻烦。

模式适用环境

  • 可以将一个需要解释执行的语言中的句子表示为一棵抽象语法树
  • 一些重复出现的问题可以用一种简单的语言来进行表达
  • 一个语言的文法较为简单
  • 执行效率不是关键问题

Iterator Pattern 迭代器模式

image-20220613203517012

  • 分析

    • 电视机<->存储电视频道的集合<->聚合类(Aggregate Classes)

    • 电视机遥控器<->操作电视频道<->迭代器(Iterator)

    • 访问一个聚合对象中的元素但又不需要暴露它的内部结构

    • 聚合对象的两个职责:

      • 存储数据,聚合对象的基本职责
      • 遍历数据,既是可变化的,又是可分离的
    • 将遍历数据的行为从聚合对象中分离出来,封装在迭代器对象中

    • 由迭代器来提供遍历聚合对象内部数据的行为,简化聚合对象的设计,更符合单一职责原则

迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,且不用暴露该对象的内部表示。

  • 又名游标(Cursor)模式
  • 通过引入迭代器,客户端无须了解聚合对象的内部结构即可实现对聚合对象中成员的遍历,还可以根据需要很方便地增加新的遍历方式
  • 迭代器模式的结构
image-20220613204418537
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
public interface Iterator {
public void first(); //将游标指向第一个元素
public void next(); //将游标指向下一个元素
public boolean hasNext(); //判断是否存在下一个元素
public Object currentItem(); //获取游标指向的当前元素
}

public class ConcreteIterator implements Iterator {
private ConcreteAggregate objects; //维持一个对具体聚合对象的引
用,以便于访问存储在聚合对象中的数据
private int cursor; //定义一个游标,用于记录当前访问位置
public ConcreteIterator(ConcreteAggregate objects) {
this.objects=objects;
}
public void first() { ...... }
public void next() { ...... }
public boolean hasNext() { ...... }
public Object currentItem() { ...... }
}

public interface Aggregate {
Iterator createIterator();
}

public class ConcreteAggregate implements Aggregate {
......
public Iterator createIterator() {
return new ConcreteIterator(this);
}
......
}
  • 结果及分析
    • 如果需要增加一个新的具体聚合类,只需增加一个新的聚合子类和一个新的具体迭代器类即可,原有类库代码无须修改,符合开闭原则
    • 需要更换一个迭代器,只需要增加一个新的具体迭代器类作为抽象迭代器类的子类,重新实现遍历方法即可,原有迭代器代码无须修改,也符合开闭原则
    • 如果要在迭代器中增加新的方法,则需要修改抽象迭代器的源代码,这将违背开闭原则
  • 宽接口VS. 窄接口
    • 宽接口:一个聚集的接口提供了可以用来修改聚集元素的方法
    • 窄接口:一个聚集的接口没有提供修改聚集元素的方法
  • 白箱聚集:聚集对象为所有对象提供同一个接口(宽接口)
    • 迭代子可以从外部控制聚集元素的迭代,控制的仅仅是一个游标—游标(Cursor)/外禀(Extrinsic)迭代子
      image-20220613205731206
  • 黑箱聚集:聚集对象为迭代子对象提供一个宽接口,而为其它对象提供一个窄接口。同时保证聚集对象的封装和迭代子功能的实现
    • 迭代子是聚集的内部类,可以自由访问聚集的元素。迭代子可以自行实现迭代功能并控制聚集元素的迭代逻辑—内禀迭代子(Intrinsic Iterator)

image-20220613210157224

黑盒聚集代码

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
public class ConcreteAggregate extends Aggregate {
public Object[] objs = { "Monk Tang", "Monkey", "Pigsy", "Sandy", "Horse" };

@Override
public Iterator createIterator() {
return new ConcreteIterator();
}

/**
* 内部成员类:具体迭代子类
*
*/
private class ConcreteIterator implements Iterator {
private int currentIndex = 0;

public void first() {
currentIndex = 0;
}

public void next() {
if (currentIndex < objs.length) {
currentIndex++;
}
}

public boolean isDone() {
return (currentIndex == objs.length);
}

public Object currentItem() {
return objs[currentIndex];
}
}
}

拥有一个私有的迭代子内部类

白盒聚集代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ConcreteAggregate extends Aggregate {
private Object[] objs = {"Monk Tang", "Monkey","Pigsy","Sandy","Horse"};

public Iterator createIterator(){
return new ConcreteIterator(this);
}

public Object getElement(int index){
if(index<objs.length){
return objs[index];
}
else{
return null;
}
}

public int size(){
return objs.length;
}

}

拥有一个开放的具体迭代子类且开放的聚集类

模式优点

  • 支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式
  • 简化了聚合类
  • 由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,符合开闭原则

模式缺点

  • 在增加新的聚合类时需要对应地增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性
  • 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是一件很容易的事情

模式适用环境

  • 访问一个聚合对象的内容而无须暴露它的内部表示
  • 需要为一个聚合对象提供多种遍历方式
  • 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口

Mediator Pattern 中介者模式

分析:

  • 星型结构:中介者模式将系统的网状结构变成以终结者为中心的星型结构,同事对象不再直接与另一个对象联系,他通过中介者对象与另一个对象发生相互作用。系统的结构不会因为新对象的引入带来大量的修改工作。
image-20220613212522045

中介者模式:定义一个对象来封装一系列对象的交互。中介者模式使各对象之间不需要显式地相互引用,从而使其耦合松散,而且让你可以独立地改变它们之间的交互。

  • 又称为调停者模式
  • 在中介者模式中,通过引入中介者来简化对象之间的复杂交互
  • 中介者模式是迪米特法则的一个典型应用
  • 对象之间多对多的复杂关系转化为相对简单的一对多关系

image-20220613212842820

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
public abstract class Mediator {
protected ArrayList<Colleague> colleagues = new
ArrayList<Colleague>(); //用于存储同事对象
//注册方法,用于增加同事对象
public void register(Colleague colleague) {
colleagues.add(colleague);
}
//声明抽象的业务方法
public abstract void operation();
}

public class ConcreteMediator extends Mediator {
//实现业务方法,封装同事之间的调用
public void operation() {
......
((Colleague)(colleagues.get(0))).method1(); //通过中介者调用同事
类的方法
......
}
}

public abstract class Colleague {
protected Mediator mediator; //维持一个抽象中介者的引用
public Colleague(Mediator mediator) {
this.mediator=mediator;
}
public abstract void method1(); //声明自身方法,处理自己的行为
//定义依赖方法,与中介者进行通信
public void method2() {
mediator.operation();
}
}

public class ConcreteColleague extends Colleague {
public ConcreteColleague(Mediator mediator) {
super(mediator);
}
//实现自身方法
public void method1() {
......
}
}

模式优点

  • 简化了对象之间的交互,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,将原本难以理解的网状结构转换成相对简单的星型结构
  • 可将各同事对象解耦
  • 可以减少子类生成,中介者模式将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使得各个同事类可被重用,无须直接对同事类进行扩展

模式缺点

  • 在具体中介者类中包含了大量的同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护

模式适用环境

  • 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解
  • 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用对象
  • 想通过一个中间类封装多个类中的行为,又不想生成太多的子类

Memento Pattern 备忘录模式

  • 分析
    • 通过使用备忘录模式可以让系统恢复到某一特定的历史状态
    • 首先保存软件系统的历史状态,当用户需要取消错误操作并且返回到某个历史状态时,可以取出事先保存的历史状态来覆盖当前状态

备忘录模式:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样就可以在以后将对象恢复到原先保存的状态。

  • 备忘录模式的定义

    • 别名为**标记(Token)**模式
    • 提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤
    • 当前在很多软件所提供的**撤销(Undo)**操作中就使用了备忘录模式
  • 备忘录模式的结构

    image-20220613213646901

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
public class Originator {
private String state;
public Originator(){}
//创建一个备忘录对象
public Memento createMemento() {
return new Memento(this);
}
//根据备忘录对象恢复原发器状态
public void restoreMemento(Memento m) {
state = m.state;
}
public void setState(String state) {
this.state=state;
}
public String getState() {
return this.state;
}
}

//备忘录类,默认可见性,包内可见
class Memento {
private String state;
Memento(Originator o) {
state = o.getState();
}
void setState(String state) {
this.state=state;
}
String getState() {
return this.state;
}
}

public class Caretaker {
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento=memento;
}
}
  • 备忘录模式的实现
    • 除了Originator类,不允许其他类来调用备忘录类Memento的构造函数与相关方法
    • 如果允许其他类调用setState()等方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义
    • 理想的情况是只允许生成该备忘录的原发器访问备忘录的内部状态

模式优点

  • 提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤
  • 实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动

模式缺点

  • 资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免地需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源

模式适用环境

  • 保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时能够恢复到先前的状态,实现撤销操作
  • 防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象

Observer Pattern 观察者模式

  • 分析
    • 软件系统:一个对象的状态或行为的变化将导致其他对象的状态或行为也发生改变,它们之间将产生联动
    • 观察者模式:
      • 定义了对象之间一种一对多的依赖关系,让一个对象的改变能够影响其他对象
      • 发生改变的对象称为观察目标,被通知的对象称为观察者
      • 一个观察目标可以对应多个观察者

观察者模式:定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象都得到通知并被自动更新

  • 别名
    • 发布-订阅(Publish/Subscribe)模式
    • 模型-视图(Model/View)模式
    • 源-监听器(Source/Listener)模式
    • 从属者模式(Dependents)模式
image-20220614163438041
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
public abstract class Subject {
//定义一个观察者集合用于存储所有观察者对象
protected ArrayList observers<Observer> = new ArrayList();
//注册方法,用于向观察者集合中增加一个观察者
public void attach(Observer observer) {
observers.add(observer);
}
//注销方法,用于在观察者集合中删除一个观察者
public void detach(Observer observer) {
observers.remove(observer);
}
//声明抽象通知方法
public abstract void notify();
}
public class ConcreteSubject extends Subject {
//实现通知方法
public void notify() {
//遍历观察者集合,调用每一个观察者的响应方法
for(Object obs:observers) {
((Observer)obs).update();
}
}
}
public interface Observer {
//声明响应方法
public void update();
}
public class ConcreteObserver implements Observer {
//实现响应方法
public void update() {
//具体响应代码
}
}

观察者与MVC

  • MVC(Model-View-Controller)架构
    • 模型(Model),视图(View)和控制器(Controller)
    • 模型可对应于观察者模式中的观察目标,视图对应于观察者控制器可充当两者之间的中介者
    • 当模型层的数据发生改变时,视图层将自动改变其显示内容

模式优点

  • 可以实现表示层和数据逻辑层的分离
  • 在观察目标和观察者之间建立一个抽象的耦合
  • 支持广播通信,简化了一对多系统设计的难度
  • 符合开闭原则,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便

模式缺点

  • 将所有的观察者都通知到会花费很多时间
  • 如果存在循环依赖时可能导致系统崩溃
  • 没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而只是知道观察目标发生了变化

适用环境

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用
  • 一个对象的改变将导致一个或多个其他对象发生改变,且并不知道具体有多少对象将发生改变,也不知道这些对象是谁
  • 需要在系统中创建一个触发链

State Pattern 状态模式

状态模式:允许一个对象在其内部状态改变时改变他的行为。对象看起来似乎修改了它的类。

  • 状态模式的定义
    • 又名状态对象(Objects for States)
    • 用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题
    • 将一个对象的状态从该对象中分离出来封装到专门的状态类中,使得对象状态可以灵活变化
    • 对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理
  • 状态模式的定义

    image-20220614164555989

状态模式包含以下3个角色:
• Context(环境类)
• State(抽象状态类)
• ConcreteState(具体状态类)

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
public abstract class State {
//声明抽象业务方法,不同的具体状态类可以有不同的实现
public abstract void handle();
}
public class ConcreteState extends State {
public void handle() {
//方法具体实现代码
}
}
public class Context {
private State state; //维持一个对抽象状态对象的引用
private int value; //其他属性值,该属性值的变化可能会导致对象的
状态发生变化
public void setState(State state) {
this.state = state;
}
public void request() {
//其他代码
state.handle(); //调用状态对象的业务方法
//其他代码
}
}
// 转换状态的实现
……
public void changeState()
{
//判断属性值,根据属性值进行状态转换
if (value == 0)
{
this.setState(new ConcreteStateA());
}
else if (value == 1)
{
this.setState(new ConcreteStateB());
}
......
}
……
// 通过切换不同的实例来做到状态的转换

模式优点

  • 封装了状态的转换规则,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中
  • 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为
  • 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,可以避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起
  • 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数

模式缺点

  • 增加系统中类和对象的个数,导致系统运行开销增大
  • 结构与实现都较为复杂,如果使用不当将导致程序结构和代码混乱,增加系统设计的难度
  • 对开闭原则的支持并不太好,增加新的状态类需要修改负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需要修改对应类的源代码

模式适用环境

  • 对象的行为依赖于它的状态(例如某些属性值),状态的改变将导致行为的变化
  • 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强

Strategy Pattern 策略模式

策略模式:定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法可以独立于使用它的客户变化。

  • 策略模式的定义

    • 又称为政策(Policy)模式
    • 每一个封装算法的类称之为**策略(Strategy)**类
    • 策略模式提供了一种可插入式(Pluggable)算法的实现方案
  • 策略模式的结构

image-20220614170230029

• Context(环境类)
• Strategy(抽象策略类)
• ConcreteStrategy(具体策略类)

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
public abstract class Strategy {
public abstract void algorithm(); //声明抽象算法
}
public class ConcreteStrategyA extends Strategy {
//算法的具体实现
public void algorithm() {
//算法A
}
}
public class Context {
private Strategy strategy; //维持一个对抽象策略类的引用
//注入策略对象
public void setStrategy(Strategy strategy) {
this.strategy= strategy;
}
//调用策略类中的算法
public void algorithm() {
strategy.algorithm();
}
}
……
Context context = new Context();
Strategy strategy;
strategy = new ConcreteStrategyA(); //可在运行时指定类型,通过配置
文件和反射机制实现
context.setStrategy(strategy);
context.algorithm();
……

模式优点

  • 提供了对开闭原则的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为
  • 提供了管理相关的算法族的办法
  • 提供了一种可以替换继承关系的办法
  • 可以避免多重条件选择语句
  • 提供了一种算法的复用机制,不同的环境类可以方便地复用策略类

模式缺点

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
  • 将造成系统产生很多具体策略类
  • 无法同时在客户端使用多个策略类

模式适用环境

  • 一个系统需要动态地在几种算法中选择一种
  • 避免使用难以维护的多重条件选择语句
  • 不希望客户端知道复杂的、与算法相关的数据结构,提高算法的保密性与安全性

Template Method Pattern 模板方法模式

模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。不满方法模式是的子类不改变一个算法结构即可重定义该算法的某些特定步骤

  • 模板方法模式的定义

    • 是一种基于继承的代码复用技术
    • 将一些复杂流程的实现步骤封装在一系列基本方法
    • 在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果
  • 模板方法模式的结构

    image-20220614184242143

    实现代码来自模板方法模式(模板方法设计模式)详解 (biancheng.net)

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
public class HookTemplateMethod {
public static void main(String[] args) {
HookAbstractClass tm = new HookConcreteClass();
tm.TemplateMethod();
}
}

//含钩子方法的抽象类
abstract class HookAbstractClass {
//模板方法
public void TemplateMethod() {
abstractMethod1();
HookMethod1();
if (HookMethod2()) {
SpecificMethod();
}
abstractMethod2();
}

//具体方法
public void SpecificMethod() {
System.out.println("抽象类中的具体方法被调用...");
}

//钩子方法1
public void HookMethod1() {
}

//钩子方法2
public boolean HookMethod2() {
return true;
}

//抽象方法1
public abstract void abstractMethod1();

//抽象方法2
public abstract void abstractMethod2();
}

//含钩子方法的具体子类
class HookConcreteClass extends HookAbstractClass {
public void abstractMethod1() {
System.out.println("抽象方法1的实现被调用...");
}

public void abstractMethod2() {
System.out.println("抽象方法2的实现被调用...");
}

public void HookMethod1() {
System.out.println("钩子方法1被重写...");
}

public boolean HookMethod2() {
return false;
}
}

通过使用钩子方法使得子类控制父类的行为。(控制调用那个抽象方法的实现)

模式优点

  • 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序
  • 提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为
  • 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行
  • 更换和增加新的子类很方便,符合单一职责原则和开闭原则

模式缺点

  • 需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统会更加庞大,设计也更加抽象(可结合桥接模式)

模式适用环境

  • 一次性实现一个算法的不变部分,并将可变的行为留给子类来实现
  • 各子类中公共的行为应被提取出来,并集中到一个公共父类中,以避免代码重复
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制

Visitor Pattern 访问者模式

  • 分析
    • 对象结构中存储了多种不同类型的对象信息
    • 对同一对象结构中的元素的操作方式并不唯一,可能需要提供多种不同的处理方式
    • 还有可能需要增加新的处理方式

访问者模式:表示一个作用于某对象结构中的各个元素的操作。访问者模式让你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

  • 访问者模式的定义

    • 它为操作存储不同类型元素的对象结构提供了一种解决方案

    • 用户可以对不同类型的元素施加不同的操作

  • 访问者模式的结构

    image-20220614190046821

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
public class VisitorPattern {
public static void main(String[] args) {
ObjectStructure os = new ObjectStructure();
os.add(new ConcreteElementA());
os.add(new ConcreteElementB());
Visitor visitor = new ConcreteVisitorA();
os.accept(visitor);
System.out.println("------------------------");
visitor = new ConcreteVisitorB();
os.accept(visitor);
}
}
//抽象访问者
interface Visitor {
void visit(ConcreteElementA element);
void visit(ConcreteElementB element);
}
//具体访问者A类
class ConcreteVisitorA implements Visitor {
public void visit(ConcreteElementA element) {
System.out.println("具体访问者A访问-->" + element.operationA());
}
public void visit(ConcreteElementB element) {
System.out.println("具体访问者A访问-->" + element.operationB());
}
}
//具体访问者B类
class ConcreteVisitorB implements Visitor {
public void visit(ConcreteElementA element) {
System.out.println("具体访问者B访问-->" + element.operationA());
}
public void visit(ConcreteElementB element) {
System.out.println("具体访问者B访问-->" + element.operationB());
}
}
//抽象元素类
interface Element {
void accept(Visitor visitor);
}
//具体元素A类
class ConcreteElementA implements Element {
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationA() {
return "具体元素A的操作。";
}
}
//具体元素B类
class ConcreteElementB implements Element {
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationB() {
return "具体元素B的操作。";
}
}
//对象结构角色
class ObjectStructure {
private List<Element> list = new ArrayList<Element>();
public void accept(Visitor visitor) {
Iterator<Element> i = list.iterator();
while (i.hasNext()) {
((Element) i.next()).accept(visitor);
}
}
public void add(Element element) {
list.add(element);
}
public void remove(Element element) {
list.remove(element);
}
}

通过遍历一种结构来获取应该调用的访问对象

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
public abstract class Node {
/**
* accept抽象接收方法
* @param visitor
*/
public abstract void accept(Visitor visitor);
}
public class NodeA extends Node {

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}

/**
* NodeA特有的商业方法
* @return
*/
public String operationA(){
return "NodeA is visited";
}

}

public class NodeB extends Node {

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}

/**
* NodeB特有的商业方法
* @return
*/
public String operationB(){
return "NodeB is visited";
}
}

public interface Visitor {
/**
* 对应于NodeA的访问操作
*/
void visit(NodeA node);

/**
* 对应于NodeB的访问操作
* @param node
*/
void visit(NodeB node);
}

public class VisitorA implements Visitor {

public void visit(NodeA node) {
System.out.println("VisitorA: "+node.operationA());
}

public void visit(NodeB node) {
System.out.println("VisitorA: "+node.operationB());

}

}

public class VisitorB implements Visitor {

public void visit(NodeA node) {
System.out.println("VisitorB: "+node.operationA());
}

public void visit(NodeB node) {
System.out.println("VisitorB: "+node.operationB());

}

}

public class ObjectStructure {

private Vector<Node> nodes;
private Node node;

public ObjectStructure(){
nodes = new Vector<Node>();
}

/**
* 执行访问操作
* @param visitor
*/
public void action(Visitor visitor){
for(Enumeration<Node> e = nodes.elements(); e.hasMoreElements();){
node = (Node)e.nextElement();
node.accept(visitor);
}
}

/**
* 增加一个新元素
* @param node
*/
public void add(Node node){
nodes.addElement(node);
}
}

public class VisitorClient {

/**
* @param args
*/
public static void main(String[] args) {
//创建一个结构对象
ObjectStructure aObjects = new ObjectStructure();
//增加节点
aObjects.add(new NodeA());
aObjects.add(new NodeB());
aObjects.add(new NodeA());
//创建一个访问者
Visitor visitor1 = new VisitorA();
//让访问者访问结构
aObjects.action(visitor1);
//创建一个新的访问者
Visitor visitor2 = new VisitorB();
//让新的访问者访问结构
aObjects.action(visitor2);
}

}

分派

以下内容来自访问者模式和双分派 - 梦中彩虹 - 博客园 (cnblogs.com)

访问者模式是一种伪动态双分派

变量被声明时的类型叫做变量的静态类型(Static Type) 又叫明显类型(Apparent Type)。变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。
**根据对象的类型而对方法进行的选择,就是分派(Dispatch)**。

根据分派发生的时期,可以将分派分为两种,即静态分派和动态分派。

静态分派和动态分派

静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。方法重载(Overload)就是静态分派的最典型的应用。(所谓的:编译时多态)

动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。面向对象的语言利用动态分派来实现方法置换产生的多态性。(所谓的:运行时多态)

也就是,运行的时候,根据参数的类型,选择合适的重载方法,就是动态分派

模式优点

  • 增加新的访问操作很方便
  • 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中,类的职责更加清晰
  • 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作

模式缺点

  • 增加新的元素类很困难
  • 破坏了对象的封装性

模式适用环境

  • 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作
  • 需要对一个对象结构中的对象进行很多不同的且不相关的操作,并需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类
  • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作