Dunwu Blog

大道至简,知易行难

设计模式之备忘录模式

意图

备忘录模式(Memento) 是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

适用场景

  • 当你需要创建对象状态快照来恢复其之前的状态时, 可以使用备忘录模式。
  • 当直接访问对象的成员变量、 获取器或设置器将导致封装被突破时, 可以使用该模式。

结构

结构说明

基于嵌套类的实现

img

  1. 原发器 (Originator) 类可以生成自身状态的快照, 也可以在需要时通过快照恢复自身状态。

  2. 备忘录 (Memento) 是原发器状态快照的值对象 (value object)。 通常做法是将备忘录设为不可变的, 并通过构造函数一次性传递数据。

  3. 负责人 (Caretaker) 仅知道 “何时” 和 “为何” 捕捉原发器的状态, 以及何时恢复状态。

    负责人通过保存备忘录栈来记录原发器的历史状态。 当原发器需要回溯历史状态时, 负责人将从栈中获取最顶部的备忘录, 并将其传递给原发器的恢复 (restoration) 方法。

  4. 在该实现方法中, 备忘录类将被嵌套在原发器中。 这样原发器就可访问备忘录的成员变量和方法, 即使这些方法被声明为私有。 另一方面, 负责人对于备忘录的成员变量和方法的访问权限非常有限: 它们只能在栈中保存备忘录, 而不能修改其状态。

基于中间接口的实现

另外一种实现方法适用于不支持嵌套类的编程语言 (没错, 我说的就是 PHP)。

img

  1. 在没有嵌套类的情况下, 你可以规定负责人仅可通过明确声明的中间接口与备忘录互动, 该接口仅声明与备忘录元数据相关的方法, 限制其对备忘录成员变量的直接访问权限。
  2. 另一方面, 原发器可以直接与备忘录对象进行交互, 访问备忘录类中声明的成员变量和方法。 这种方式的缺点在于你需要将备忘录的所有成员变量声明为公有。

封装更加严格的实现

如果你不想让其他类有任何机会通过备忘录来访问原发器的状态, 那么还有另一种可用的实现方式。

img

  1. 这种实现方式允许存在多种不同类型的原发器和备忘录。 每种原发器都和其相应的备忘录类进行交互。 原发器和备忘录都不会将其状态暴露给其他类。
  2. 负责人此时被明确禁止修改存储在备忘录中的状态。 但负责人类将独立于原发器, 因为此时恢复方法被定义在了备忘录类中。
  3. 每个备忘录将与创建了自身的原发器连接。 原发器会将自己及状态传递给备忘录的构造函数。 由于这些类之间的紧密联系, 只要原发器定义了合适的设置器 (setter), 备忘录就能恢复其状态。

结构代码范式

Memento : 负责存储 Originator 对象的内部状态,并可以防止 Originator 以外的其他对象访问 Memento。

Memento 有两个接口,Caretaker 只能看到备忘录的窄接口,它只能将备忘录传递给其他对象。
Originator 可以看到一个宽接口,允许它访问返回到先前状态所需的所有数据。

1
2
3
4
5
6
7
8
9
10
class Memento {
private String state;
public Memento(String state) {
this.state = state;
}

public String GetState() {
return state;
}
}

Originator : 负责创建一个备忘录 Memento,用以记录当前时刻它的内部状态,并可使用备忘录恢复内部状态。

Originator 可根据需要决定 Memento 存储 Originator 的哪些内部状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Originator {
private String state;

public void SetState(String state) {
this.state = state;
}
public String GetState() {
return state;
}

public Memento CreateMemento() {
return (new Memento(state));
}

public void SetMemento(Memento memento) {
state = memento.GetState();
}

public void Show() {
System.out.println("State = " + state);
}
}

Caretaker : 负责保存好备忘录 Memento,不能对备忘录的内容进行操作或检查。

1
2
3
4
5
6
7
8
9
10
class Caretaker {
private Memento memento;

public void SetMemento(Memento memento) {
this.memento = memento;
}
public Memento GetMemento() {
return memento;
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MementoPattern {
public static void main(String[] args) {
Originator o = new Originator();
o.SetState("ON");
o.Show();

Caretaker c = new Caretaker();
c.SetMemento(o.CreateMemento());

o.SetState("OFF");
o.Show();

o.SetMemento(c.GetMemento());
o.Show();
}
}

输出

1
2
3
State = ON
State = OFF
State = ON

伪代码

本例结合使用了命令模式与备忘录模式, 可保存复杂文字编辑器的状态快照, 并能在需要时从快照中恢复之前的状态。

img

命令 (command) 对象将作为负责人, 它们会在执行与命令相关的操作前获取编辑器的备忘录。 当用户试图撤销最近的命令时, 编辑器可以使用保存在命令中的备忘录来将自身回滚到之前的状态。

备忘录类没有声明任何公有的成员变量、 获取器 (getter) 和设置器, 因此没有对象可以修改其内容。 备忘录与创建自己的编辑器相连接, 这使得备忘录能够通过编辑器对象的设置器传递数据, 恢复与其相连接的编辑器的状态。 由于备忘录与特定的编辑器对象相连接, 程序可以使用中心化的撤销栈实现对多个独立编辑器窗口的支持。

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
// 原发器中包含了一些可能会随时间变化的重要数据。它还定义了在备忘录中保存
// 自身状态的方法,以及从备忘录中恢复状态的方法。
class Editor is
private field text, curX, curY, selectionWidth

method setText(text) is
this.text = text

method setCursor(x, y) is
this.curX = curX
this.curY = curY

method setSelectionWidth(width) is
this.selectionWidth = width

// 在备忘录中保存当前的状态。
method createSnapshot():Snapshot is
// 备忘录是不可变的对象;因此原发器会将自身状态作为参数传递给备忘
// 录的构造函数。
return new Snapshot(this, text, curX, curY, selectionWidth)

// 备忘录类保存有编辑器的过往状态。
class Snapshot is
private field editor: Editor
private field text, curX, curY, selectionWidth

constructor Snapshot(editor, text, curX, curY, selectionWidth) is
this.editor = editor
this.text = text
this.curX = curX
this.curY = curY
this.selectionWidth = selectionWidth

// 在某一时刻,编辑器之前的状态可以使用备忘录对象来恢复。
method restore() is
editor.setText(text)
editor.setCursor(curX, curY)
editor.setSelectionWidth(selectionWidth)

// 命令对象可作为负责人。在这种情况下,命令会在修改原发器状态之前获取一个
// 备忘录。当需要撤销时,它会从备忘录中恢复原发器的状态。
class Command is
private field backup: Snapshot

method makeBackup() is
backup = editor.createSnapshot()

method undo() is
if (backup != null)
backup.restore()
// ...

与其他模式的关系

  • 你可以同时使用命令模式备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。
  • 你可以同时使用备忘录迭代器模式来获取当前迭代器的状态, 并且在需要的时候进行回滚。
  • 有时候原型模式可以作为备忘录的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。

案例

使用示例: 备忘录的基本原则可通过序列化来实现, 这在 Java 语言中很常见。 尽管备忘录不是生成对象状态快照的唯一或最有效方法, 但它能在保护原始对象的结构不暴露给其他对象的情况下保存对象状态的备份。

下面是核心 Java 程序库中该模式的一些示例:

参考资料

设计模式之职责链模式

意图

职责链模式(Chain Of Responsibility) 是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。
将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

适用场景

  • 当程序需要使用不同方式处理不同种类请求, 而且请求类型和顺序预先未知时, 可以使用责任链模式。
  • 当必须按顺序执行多个处理者时, 可以使用该模式。
  • 如果所需处理者及其顺序必须在运行时进行改变, 可以使用责任链模式。

结构

结构说明

img

  1. 处理者 (Handler) 声明了所有具体处理者的通用接口。 该接口通常仅包含单个方法用于请求处理, 但有时其还会包含一个设置链上下个处理者的方法。

  2. 基础处理者 (Base Handler) 是一个可选的类, 你可以将所有处理者共用的样本代码放置在其中。

    通常情况下, 该类中定义了一个保存对于下个处理者引用的成员变量。 客户端可通过将处理者传递给上个处理者的构造函数或设定方法来创建链。 该类还可以实现默认的处理行为: 确定下个处理者存在后再将请求传递给它。

  3. 具体处理者 (Concrete Handlers) 包含处理请求的实际代码。 每个处理者接收到请求后, 都必须决定是否进行处理, 以及是否沿着链传递请求。

    处理者通常是独立且不可变的, 需要通过构造函数一次性地获得所有必要地数据。

  4. 客户端 (Client) 可根据程序逻辑一次性或者动态地生成链。 值得注意的是, 请求可发送给链上的任意一个处理者, 而非必须是第一个处理者。

结构代码范式

Handler : 定义一个处理请求的接口。(**可选的**)实现设置后继者的方法。

1
2
3
4
5
6
7
8
abstract class Handler {
protected Handler successor;
public void SetSuccesssor(Handler successor) {
this.successor = successor;
}

public abstract void HandlerRequest(int request);
}

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
class ConcreteHandler1 extends Handler {
@Override
public void HandlerRequest(int request) {
if (request >= 0 && request < 10) {
System.out.println("ConcreteHandler1 处理请求 " + request);
} else if (null != successor) {
successor.HandlerRequest(request);
}
}
}

class ConcreteHandler2 extends Handler {
@Override
public void HandlerRequest(int request) {
if (request >= 10 && request < 20) {
System.out.println("ConcreteHandler2 处理请求 " + request);
} else if (null != successor) {
successor.HandlerRequest(request);
}
}
}

class ConcreteHandler3 extends Handler {
@Override
public void HandlerRequest(int request) {
if (request >= 20 && request < 30) {
System.out.println("ConcreteHandler3 处理请求 " + request);
} else if (null != successor) {
successor.HandlerRequest(request);
}
}
}

Client : 需要设置一个职责链的各环节对象串联起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ChainOfResponsibilityPattern {
public static void main(String[] args) {
Handler h1 = new ConcreteHandler1();
Handler h2 = new ConcreteHandler2();
Handler h3 = new ConcreteHandler3();
h1.SetSuccesssor(h2);
h2.SetSuccesssor(h3);

int[] requests = {2, 29, 9, 15, 4, 19};
for (int i : requests) {
h1.HandlerRequest(i);
}
}
}

伪代码

在本例中, 责任链模式负责为活动的 GUI 元素显示上下文帮助信息。

img

应用程序的 GUI  通常为对象树结构。 例如, 负责渲染程序主窗口的 对话框类就是对象树的根节点。 对话框包含 面板 , 而面板可能包含其他面板, 或是 按钮文本框等下层元素。

只要给一个简单的组件指定帮助文本, 它就可显示简短的上下文提示。 但更复杂的组件可自定义上下文帮助文本的显示方式, 例如显示手册摘录内容或在浏览器中打开一个网页。

img

当用户将鼠标指针移动到某个元素并按下 F1键时, 程序检测到指针下的组件并对其发送帮助请求。 该请求不断向上传递到该元素所有的容器, 直至某个元素能够显示帮助信息。

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
// 处理者接口声明了一个创建处理者链的方法。还声明了一个执行请求的方法。
interface ComponentWithContextualHelp is
method showHelp()


// 简单组件的基础类。
abstract class Component implements ComponentWithContextualHelp is
field tooltipText: string

// 组件容器在处理者链中作为“下一个”链接。
protected field container: Container

// 如果组件设定了帮助文字,那它将会显示提示信息。如果组件没有帮助文字
// 且其容器存在,那它会将调用传递给容器。
method showHelp() is
if (tooltipText != null)
// 显示提示信息。
else
container.showHelp()


// 容器可以将简单组件和其他容器作为其子项目。链关系将在这里建立。该类将从
// 其父类处继承 showHelp(显示帮助)的行为。
abstract class Container extends Component is
protected field children: array of Component

method add(child) is
children.add(child)
child.container = this


// 原始组件应该能够使用帮助操作的默认实现...
class Button extends Component is
// ...

// 但复杂组件可能会对默认实现进行重写。如果无法以新的方式来提供帮助文字,
// 那组件总是还能调用基础实现的(参见 Component 类)。
class Panel extends Container is
field modalHelpText: string

method showHelp() is
if (modalHelpText != null)
// 显示包含帮助文字的模态窗口。
else
super.showHelp()

// ...同上...
class Dialog extends Container is
field wikiPageURL: string

method showHelp() is
if (wikiPageURL != null)
// 打开百科帮助页面。
else
super.showHelp()


// 客户端代码。
class Application is
// 每个程序都能以不同方式对链进行配置。
method createUI() is
dialog = new Dialog("预算报告")
dialog.wikiPageURL = "http://..."
panel = new Panel(0, 0, 400, 800)
panel.modalHelpText = "本面板用于..."
ok = new Button(250, 760, 50, 20, "确认")
ok.tooltipText = "这是一个确认按钮..."
cancel = new Button(320, 760, 50, 20, "取消")
// ...
panel.add(ok)
panel.add(cancel)
dialog.add(panel)

// 想象这里会发生什么。
method onF1KeyPress() is
component = this.getComponentAtMouseCoords()
component.showHelp()

与其他模式的关系

  • 责任链模式命令模式中介者模式观察者模式用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 责任链通常和组合模式结合使用。 在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。

  • 责任链的管理者可使用命令模式实现。 在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。

    还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。

  • 责任链装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。

    责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。

案例

使用示例: 责任链模式在 Java 程序中并不常见, 因为它仅在代码与对象链打交道时才能发挥作用。

该模式最流行的使用案例之一是在 GUI 类中将事件向上传递给父组件。 另一个值得注意的使用案例是依次访问过滤器。

下面是该模式在核心 Java 程序库中的一些示例:

识别方法: 该模式可通过一组对象的行为方法间接调用其他对象的相同方法来识别, 而且所有对象都会遵循相同的接口。

参考资料

设计模式之中介者模式

意图

中介者模式(Mediator) 是一种行为设计模式, 能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。

适用场景

  • 当一些对象和其他对象紧密耦合以致难以对其进行修改时, 可使用中介者模式。
  • 当组件因过于依赖其他组件而无法在不同应用中复用时, 可使用中介者模式。
  • 如果为了能在不同情景下复用一些基本行为, 导致你需要被迫创建大量组件子类时, 可使用中介者模式。

结构

img

结构说明

  1. 组件 (Component) 是各种包含业务逻辑的类。 每个组件都有一个指向中介者的引用, 该引用被声明为中介者接口类型。 组件不知道中介者实际所属的类, 因此你可通过将其连接到不同的中介者以使其能在其他程序中复用。
  2. 中介者 (Mediator) 接口声明了与组件交流的方法, 但通常仅包括一个通知方法。 组件可将任意上下文 (包括自己的对象) 作为该方法的参数, 只有这样接收组件和发送者类之间才不会耦合。
  3. 具体中介者 (Concrete Mediator) 封装了多种组件间的关系。 具体中介者通常会保存所有组件的引用并对其进行管理, 甚至有时会对其生命周期进行管理。
  4. 组件并不知道其他组件的情况。 如果组件内发生了重要事件, 它只能通知中介者。 中介者收到通知后能轻易地确定发送者, 这或许已足以判断接下来需要触发的组件了。
    • 对于组件来说, 中介者看上去完全就是一个黑箱。 发送者不知道最终会由谁来处理自己的请求, 接收者也不知道最初是谁发出了请求。

结构代码范式

Mediator : 为 Colleague 对象定义一个交流接口。

1
2
3
abstract class Mediator {
public abstract void Send(String message, Colleague colleague);
}

ConcreteMediator : 实现 Mediator 中的交流接口。 这个类中需要了解并维护所有的 colleague 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ConcreteMediator extends Mediator {
private ConcreteColleague1 colleague1;
private ConcreteColleague2 colleague2;

public void setColleague1(ConcreteColleague1 colleague1) {
this.colleague1 = colleague1;
}

public void setColleague2(ConcreteColleague2 colleague2) {
this.colleague2 = colleague2;
}

@Override
public void Send(String message, Colleague colleague) {
if (colleague == colleague1) {
colleague2.Notify(message);
} else if (colleague == colleague2){
colleague1.Notify(message);
} else {
System.out.println("Error!");
}
}
}

Colleague 组 : 每个 Colleague 对象应该知道它的 Mediator 对象,但不知道其他同事对象。它只能联系 Mediator 对象。

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
abstract class Colleague {
protected Mediator mediator;

public Colleague(Mediator mediator) {
this.mediator = mediator;
}

public void Send(String message) {
mediator.Send(message, this);
}

public abstract void Notify(String message);
}

class ConcreteColleague1 extends Colleague {
public ConcreteColleague1(Mediator mediator) {
super(mediator);
}

@Override
public void Notify(String message) {
System.out.println("同事1得到信息:" + message);
}
}

class ConcreteColleague2 extends Colleague {
public ConcreteColleague2(Mediator mediator) {
super(mediator);
}

@Override
public void Notify(String message) {
System.out.println("同事2得到信息:" + message);
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MediatorPattern {
public static void main(String[] args) {
ConcreteMediator mediator = new ConcreteMediator();
ConcreteColleague1 colleague1 = new ConcreteColleague1(mediator);
ConcreteColleague2 colleague2 = new ConcreteColleague2(mediator);

mediator.setColleague1(colleague1);
mediator.setColleague2(colleague2);

colleague1.Send("How are you?");
colleague2.Send("Fine, thank you. And you?");
colleague1.Send("I'm fine. Thankes.");
}
}

输出

1
2
3
同事2得到信息:How are you?
同事1得到信息:Fine, thank you. And you?
同事2得到信息:I'm fine. Thankes.

伪代码

在本例中, 中介者模式可帮助你减少各种 UI 类 (按钮、 复选框和文本标签) 之间的相互依赖关系。

img

用户触发的元素不会直接与其他元素交流, 即使看上去它们应该这样做。 相反, 元素只需让中介者知晓事件即可, 并能在发出通知时同时传递任何上下文信息。

本例中的中介者是整个认证对话框。 对话框知道具体元素应如何进行合作并促进它们的间接交流。 当接收到事件通知后, 对话框会确定负责处理事件的元素并据此重定向请求。

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
// 中介者接口声明了一个能让组件将各种事件通知给中介者的方法。中介者可对这
// 些事件做出响应并将执行工作传递给其他组件。
interface Mediator is
method notify(sender: Component, event: string)


// 具体中介者类可解开各组件之间相互交叉的连接关系并将其转移到中介者中。
class AuthenticationDialog implements Mediator is
private field title: string
private field loginOrRegisterChkBx: Checkbox
private field loginUsername, loginPassword: Textbox
private field registrationUsername, registrationPassword,
registrationEmail: Textbox
private field okBtn, cancelBtn: Button

constructor AuthenticationDialog() is
// 创建所有组件对象并将当前中介者传递给其构造函数以建立连接。

// 当组件中有事件发生时,它会通知中介者。中介者接收到通知后可自行处理,
// 也可将请求传递给另一个组件。
method notify(sender, event) is
if (sender == loginOrRegisterChkBx and event == "check")
if (loginOrRegisterChkBx.checked)
title = "登录"
// 1. 显示登录表单组件。
// 2. 隐藏注册表单组件。
else
title = "注册"
// 1. 显示注册表单组件。
// 2. 隐藏登录表单组件。

if (sender == okBtn && event == "click")
if (loginOrRegister.checked)
// 尝试找到使用登录信息的用户。
if (!found)
// 在登录字段上方显示错误信息。
else
// 1. 使用注册字段中的数据创建用户账号。
// 2. 完成用户登录工作。 …


// 组件会使用中介者接口与中介者进行交互。因此只需将它们与不同的中介者连接
// 起来,你就能在其他情境中使用这些组件了。
class Component is
field dialog: Mediator

constructor Component(dialog) is
this.dialog = dialog

method click() is
dialog.notify(this, "click")

method keypress() is
dialog.notify(this, "keypress")

// 具体组件之间无法进行交流。它们只有一个交流渠道,那就是向中介者发送通知。
class Button extends Component is
// ...

class Textbox extends Component is
// ...

class Checkbox extends Component is
method check() is
dialog.notify(this, "check")
// ...

与其他模式的关系

  • 责任链模式命令模式中介者模式观察者模式用于处理请求发送者和接收者之间的不同连接方式:
    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 外观模式中介者的职责类似: 它们都尝试在大量紧密耦合的类中组织起合作。
    • 外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。
    • 中介者将系统中组件的沟通行为中心化。 各组件只知道中介者对象, 无法直接相互交流。
  • 中介者观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来看看如何做到这一点。
    • 中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。
    • 有一种流行的中介者模式实现方式依赖于观察者。 中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。 当中介者以这种方式实现时, 它可能看上去与观察者非常相似。
    • 当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例如, 你可永久性地将所有组件链接到同一个中介者对象。 这种实现方式和观察者并不相同, 但这仍是一种中介者模式。
    • 假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。 这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者。

案例

使用示例: 中介者模式在 Java 代码中最常用于帮助程序 GUI 组件之间的通信。 在 MVC 模式中, 控制器是中介者的同义词。

下面是核心 Java 程序库中该模式的一些示例:

参考资料

设计模式之解释器模式

简介

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

解释器模式是一种行为型模式

img

Context : 包含解释器之外的一些全局信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

class Context {
private String input;
private String output;

public void setInput(String input) {
this.input = input;
}

public String getInput() {
return this.input;
}

public void setOutput(String output) {
this.output = output;
}

public String getOutput() {
return this.output;
}
}

AbstractExpression : 声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点所共享。

1
2
3
abstract class AbstractExpression {
public abstract void Interpret(Context context);
}

TerminalExpression : 实现与文法中的终结符相关联的解释操作。实现抽象表达式中所要求的接口,主要是一个 Interprete()方法。

文法中的每一个终结符都有一个具体终结表达式与之对应。

1
2
3
4
5
6
7
class TerminalExpression extends AbstractExpression {
@Override
public void Interpret(Context context) {
context.setOutput("终端" + context.getInput());
System.out.println(context.getInput() + "经过终端解释器解释为:" + context.getOutput());
}
}

NonterminalExpression : 实现与文法中的非终结符相关联的解释操作。对文法中的每一条规则 R1,R2……Rn 都需要一个具体的非终结符表达式类。通过实现抽象表达式的 Interpret 方法实现解释操作。

1
2
3
4
5
6
7
class NonterminalExpression extends AbstractExpression {
@Override
public void Interpret(Context context) {
context.setOutput("非终端" + context.getInput());
System.out.println(context.getInput() + "经过非终端解释器解释为:" + context.getOutput());
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
public class InterpreterPattern {
public static void main(String[] args) {
Context context = new Context();
context.setInput("ABC");

AbstractExpression expression1 = new TerminalExpression();
expression1.Interpret(context);

AbstractExpression expression2 = new NonterminalExpression();
expression2.Interpret(context);
}
}

输出

1
2
ABC经过终端解释器解释为:终端ABC
ABC经过非终端解释器解释为:非终端ABC

实例

场景

参考资料

设计模式之观察者模式

意图

观察者模式(Observer)是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。

适用场景

  • 当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式。
  • 当应用中的一些对象必须观察其他对象时, 可使用该模式。 但仅能在有限时间内或特定情况下使用。

结构

img

结构说明

  1. 发布者 (Publisher) 会向其他对象发送值得关注的事件。 事件会在发布者自身状态改变或执行特定行为后发生。 发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。
  2. 当新事件发生时, 发送者会遍历订阅列表并调用每个订阅者对象的通知方法。 该方法是在订阅者接口中声明的。
  3. 订阅者 (Subscriber) 接口声明了通知接口。 在绝大多数情况下, 该接口仅包含一个 update更新方法。 该方法可以拥有多个参数, 使发布者能在更新时传递事件的详细信息。
  4. 具体订阅者 (Concrete Subscribers) 可以执行一些操作来回应发布者的通知。 所有具体订阅者类都实现了同样的接口, 因此发布者不需要与具体类相耦合。
  5. 订阅者通常需要一些上下文信息来正确地处理更新。 因此, 发布者通常会将一些上下文数据作为通知方法的参数进行传递。 发布者也可将自身作为参数进行传递, 使订阅者直接获取所需的数据。
  6. 客户端 (Client) 会分别创建发布者和订阅者对象, 然后为订阅者注册发布者更新。

结构代码范式

Subject : 主题类,保存所有订阅此主题的观察者,观察者的 数量是任意的。定义 添加观察者 (Attach)删除观察者 (Detach) 的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class Subject {
protected String name;
protected String state;
protected List<Observer> observers = new ArrayList<Observer>();

public abstract String getState();
public abstract void setState(String state);
public abstract void Notify();

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

public void Attach(Observer observer) {
observers.add(observer);
}

public void Detach(Observer observer) {
observers.remove(observer);
}
}

Observer : 观察者类,定义**更新接口 (Update)**,当收到 Subject 的通知时,Observer 需要同步更新信息。

1
2
3
4
5
6
7
8
9
abstract class Observer {
protected String name;
protected Subject subject;
public Observer(String name, Subject subject) {
this.name = name;
this.subject = subject;
}
public abstract void Update();
}

ConcreteSubject : 具体主题类,存储对于这个主题感兴趣的所有观察者。当内部状态发生变化时,应**通知所有登记的观察者(Notify)**。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ConcreteSubject extends Subject {
public ConcreteSubject(String name) {
super(name);
}

@Override
public String getState() {
return state;
}

@Override
public void setState(String state) {
this.state = state;
}

@Override
public void Notify() {
System.out.println("======= " + this.name + "主题发布新消息 =======");
for (Observer observer : observers) {
observer.Update();
}
}
}

ConcreteObserver : 具体观察者类,实现 Observer 的**更新接口 (Update)**,以便和 Subject 同步状态信息。

1
2
3
4
5
6
7
8
9
10
11
12
class ConcreteObserver extends Observer {
private String state;
public ConcreteObserver(String name, Subject subject) {
super(name, subject);
}

@Override
public void Update() {
state = subject.getState();
System.out.println(this.name + "收到当前状态:" + state);
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ObserverPattern {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject("天气");
ConcreteObserver observer1 = new ConcreteObserver("张三", subject);
ConcreteObserver observer2 = new ConcreteObserver("李四", subject);
ConcreteObserver observer3 = new ConcreteObserver("王五", subject);

subject.Attach(observer1);
subject.Attach(observer2);
subject.Attach(observer3);
subject.setState("今天下雨");
subject.Notify();

subject.Detach(observer2);
subject.setState("明天天晴");
subject.Notify();
}
}

输出

1
2
3
4
5
6
7
======= 天气主题发布新消息 =======
张三收到当前状态:今天下雨
李四收到当前状态:今天下雨
王五收到当前状态:今天下雨
======= 天气主题发布新消息 =======
张三收到当前状态:明天天晴
王五收到当前状态:明天天晴

伪代码

在本例中, 观察者模式允许文本编辑器对象将自身的状态改变通知给其他服务对象。

img

订阅者列表是动态生成的: 对象可在运行时根据程序需要开始或停止监听通知。

在本实现中, 编辑器类自身并不维护订阅列表。 它将工作委派给专门从事此工作的一个特殊帮手对象。 你还可将该对象升级为中心化的事件分发器, 允许任何对象成为发布者。

只要发布者通过同样的接口与所有订阅者进行交互, 那么在程序中新增订阅者时就无需修改已有发布者类的代码。

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
// 发布者基类包含订阅管理代码和通知方法。
class EventManager is
private field listeners: hash map of event types and listeners

method subscribe(eventType, listener) is
listeners.add(eventType, listener)

method unsubscribe(eventType, listener) is
listeners.remove(eventType, listener)

method notify(eventType, data) is
foreach (listener in listeners.of(eventType)) do
listener.update(data)

// 具体发布者包含一些订阅者感兴趣的实际业务逻辑。我们可以从发布者基类中扩
// 展出该类,但在实际情况下并不总能做到,因为具体发布者可能已经是子类了。
// 在这种情况下,你可用组合来修补订阅逻辑,就像我们在这里做的一样。
class Editor is
public field events: EventManager
private field file: File

constructor Editor() is
events = new EventManager()

// 业务逻辑的方法可将变化通知给订阅者。
method openFile(path) is
this.file = new File(path)
events.notify("open", file.name)

method saveFile() is
file.write()
events.notify("save", file.name)

// ...


// 这里是订阅者接口。如果你的编程语言支持函数类型,则可用一组函数来代替整
// 个订阅者的层次结构。
interface EventListener is
method update(filename)

// 具体订阅者会对其注册的发布者所发出的更新消息做出响应。
class LoggingListener implements EventListener is
private field log: File
private field message

constructor LoggingListener(log_filename, message) is
this.log = new File(log_filename)
this.message = message

method update(filename) is
log.write(replace('%s',filename,message))

class EmailAlertsListener implements EventListener is
private field email: string

constructor EmailAlertsListener(email, message) is
this.email = email
this.message = message

method update(filename) is
system.email(email, replace('%s',filename,message))


// 应用程序可在运行时配置发布者和订阅者。
class Application is
method config() is
editor = new Editor()

logger = new LoggingListener(
"/path/to/log.txt",
"有人打开了文件:%s");
editor.events.subscribe("open", logger)

emailAlerts = new EmailAlertsListener(
"admin@example.com",
"有人更改了文件:%s")
editor.events.subscribe("save", emailAlerts)

与其他模式的关系

  • 责任链模式命令模式中介者模式观察者模式用于处理请求发送者和接收者之间的不同连接方式:
    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 中介者观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来看看如何做到这一点。
    • 中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。
    • 有一种流行的中介者模式实现方式依赖于观察者。 中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。 当中介者以这种方式实现时, 它可能看上去与观察者非常相似。
    • 当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例如, 你可永久性地将所有组件链接到同一个中介者对象。 这种实现方式和观察者并不相同, 但这仍是一种中介者模式。
    • 假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。 这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者。

案例

使用示例: 观察者模式在 Java 代码中很常见, 特别是在 GUI 组件中。 它提供了在不与其他对象所属类耦合的情况下对其事件做出反应的方式。

这里是核心 Java 程序库中该模式的一些示例:

识别方法: 该模式可以通过将对象存储在列表中的订阅方法, 和对于面向该列表中对象的更新方法的调用来识别。

参考资料

设计模式之命令模式

意图

命令模式(Command) 是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。

命令模式的交互

img

  • Client 创建一个 ConcreteCommand 对象并指定他的 Receiver 对象。
  • 某个 Invoker 对象存储该 ConcreteCommand 对象。
  • 该 Invoker 通过调用 Command 对象的 Execute 操作来提交一个请求。若该命令是可撤销的,ConcreteCommand 就在执行 Execute 操作之前存储当前状态以用于取消该命令。
  • ConcreteCommand 对象对调用它的 Receiver 的一些操作以执行该请求。

命令模式的要点

  • 命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
  • 每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作。
  • 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。
  • 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
  • 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。

适用场景

  • 如果你需要通过操作来参数化对象, 可使用命令模式。
  • 如果你想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式。
  • 如果你想要实现操作回滚功能, 可使用命令模式。

结构

img

结构说明

  1. 发送者 (Sender)——亦称 “触发者 (Invoker)”——类负责对请求进行初始化, 其中必须包含一个成员变量来存储对于命令对象的引用。 发送者触发命令, 而不向接收者直接发送请求。 注意, 发送者并不负责创建命令对象: 它通常会通过构造函数从客户端处获得预先生成的命令。

  2. 命令 (Command) 接口通常仅声明一个执行命令的方法。

  3. 具体命令 (Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务逻辑对象。 但为了简化代码, 这些类可以进行合并。

    接收对象执行方法所需的参数可以声明为具体命令的成员变量。 你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化。

  4. 接收者 (Receiver) 类包含部分业务逻辑。 几乎任何对象都可以作为接收者。 绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作。

  5. 客户端 (Client) 会创建并配置具体命令对象。 客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。 此后, 生成的命令就可以与一个或多个发送者相关联了。

结构代码范式

Command : 用来声明执行操作的接口。

1
2
3
4
5
6
7
8
abstract class Command {
protected Receiver receiver;
public Command(Receiver receiver) {
this.receiver = receiver;
}

public abstract void Execute();
}

ConcreteCommand : 将一个接收者对象绑定一个动作,调用接收者相应的操作,以实现 Execute。

1
2
3
4
5
6
7
8
9
10
class ConcreteCommand extends Command {
public ConcreteCommand(Receiver receiver) {
super(receiver);
}

@Override
public void Execute() {
receiver.Action();
}
}

Invoker : 要求该命令执行这个请求。

1
2
3
4
5
6
7
8
9
10
11
class Invoker {
private Command command;

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

public void ExecuteCommand() {
command.Execute();
}
}

Receiver : 知道如何实施与执行一个与请求相关的操作,任何类都可能作为一个接收者。

1
2
3
4
5
class Receiver {
public void Action() {
System.out.println("执行请求");
}
}

Client : 创建一个具体命令对象并设定它的接受者。

1
2
3
4
5
6
7
8
public class CommandPattern {
public static void main(String[] args) {
Receiver receiver = new Receiver();
Command cmd = new ConcreteCommand(receiver);
Invoker invoker = new Invoker(cmd);
invoker.ExecuteCommand();
}
}

伪代码

在本例中, 命令模式会记录已执行操作的历史记录, 以在需要时撤销操作。

img

有些命令会改变编辑器的状态 (例如剪切和粘贴), 它们可在执行相关操作前对编辑器的状态进行备份。 命令执行后会和当前点备份的编辑器状态一起被放入命令历史 (命令对象栈)。 此后, 如果用户需要进行回滚操作, 程序可从历史记录中取出最近的命令, 读取相应的编辑器状态备份, 然后进行恢复。

客户端代码 (GUI 元素和命令历史等) 没有和具体命令类相耦合, 因为它通过命令接口来使用命令。 这使得你能在无需修改已有代码的情况下在程序中增加新的命令。

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
// 命令基类会为所有具体命令定义通用接口。
abstract class Command is
protected field app: Application
protected field editor: Editor
protected field backup: text

constructor Command(app: Application, editor: Editor) is
this.app = app
this.editor = editor

// 备份编辑器状态。
method saveBackup() is
backup = editor.text

// 恢复编辑器状态。
method undo() is
editor.text = backup

// 执行方法被声明为抽象以强制所有具体命令提供自己的实现。该方法必须根
// 据命令是否更改编辑器的状态返回 true 或 false。
abstract method execute()


// 这里是具体命令。
class CopyCommand extends Command is
// 复制命令不会被保存到历史记录中,因为它没有改变编辑器的状态。
method execute() is
app.clipboard = editor.getSelection()
return false

class CutCommand extends Command is
// 剪切命令改变了编辑器的状态,因此它必须被保存到历史记录中。只要方法
// 返回 true,它就会被保存。
method execute() is
saveBackup()
app.clipboard = editor.getSelection()
editor.deleteSelection()
return true

class PasteCommand extends Command is
method execute() is
saveBackup()
editor.replaceSelection(app.clipboard)
return true

// 撤销操作也是一个命令。
class UndoCommand extends Command is
method execute() is
app.undo()
return false


// 全局命令历史记录就是一个堆桟。
class CommandHistory is
private field history: array of Command

// 后进...
method push(c: Command) is
// 将命令压入历史记录数组的末尾。

// ...先出
method pop():Command is
// 从历史记录中取出最近的命令。


// 编辑器类包含实际的文本编辑操作。它会担任接收者的角色:最后所有命令都会
// 将执行工作委派给编辑器的方法。
class Editor is
field text: string

method getSelection() is
// 返回选中的文字。

method deleteSelection() is
// 删除选中的文字。

method replaceSelection(text) is
// 在当前位置插入剪贴板中的内容。

// 应用程序类会设置对象之间的关系。它会担任发送者的角色:当需要完成某些工
// 作时,它会创建并执行一个命令对象。
class Application is
field clipboard: string
field editors: array of Editors
field activeEditor: Editor
field history: CommandHistory

// 将命令分派给 UI 对象的代码可能会是这样的。
method createUI() is
// ...
copy = function() { executeCommand(
new CopyCommand(this, activeEditor)) }
copyButton.setCommand(copy)
shortcuts.onKeyPress("Ctrl+C", copy)

cut = function() { executeCommand(
new CutCommand(this, activeEditor)) }
cutButton.setCommand(cut)
shortcuts.onKeyPress("Ctrl+X", cut)

paste = function() { executeCommand(
new PasteCommand(this, activeEditor)) }
pasteButton.setCommand(paste)
shortcuts.onKeyPress("Ctrl+V", paste)

undo = function() { executeCommand(
new UndoCommand(this, activeEditor)) }
undoButton.setCommand(undo)
shortcuts.onKeyPress("Ctrl+Z", undo)

// 执行一个命令并检查它是否需要被添加到历史记录中。
method executeCommand(command) is
if (command.execute)
history.push(command)

// 从历史记录中取出最近的命令并运行其 undo(撤销)方法。请注意,你并
// 不知晓该命令所属的类。但是我们不需要知晓,因为命令自己知道如何撤销
// 其动作。
method undo() is
command = history.pop()
if (command != null)
command.undo()

与其他模式的关系

  • 责任链模式命令模式中介者模式观察者模式用于处理请求发送者和接收者之间的不同连接方式:
    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 责任链的管理者可使用命令模式实现。 在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。
    还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。
  • 你可以同时使用命令备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。
  • 命令策略模式看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。
    • 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。
    • 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。
  • 原型模式可用于保存命令的历史记录。
  • 你可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。

案例

使用示例:命令模式在 Java 代码中很常见。 大部分情况下, 它被用于代替包含行为的参数化 UI 元素的回调函数, 此外还被用于对任务进行排序和记录操作历史记录等。

以下是在核心 Java 程序库中的一些示例:

识别方法: 命令模式可以通过抽象或接口类型 (发送者) 中的行为方法来识别, 该类型调用另一个不同的抽象或接口类型 (接收者) 实现中的方法, 该实现则是在创建时由命令模式的实现封装。 命令类通常仅限于一些特殊行为。

参考资料

设计模式之迭代器模式

意图

迭代器模式(Iterator) 是一种行为设计模式, 让你能在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素。

适用场景

  • 当集合背后为复杂的数据结构, 且你希望对客户端隐藏其复杂性时 (出于使用便利性或安全性的考虑), 可以使用迭代器模式。
  • 使用该模式可以减少程序中重复的遍历代码。
  • 如果你希望代码能够遍历不同的甚至是无法预知的数据结构, 可以使用迭代器模式。

结构

img

结构说明

  1. 迭代器 (Iterator) 接口声明了遍历集合所需的操作: 获取下一个元素、 获取当前位置和重新开始迭代等。
  2. 具体迭代器 (Concrete Iterators) 实现遍历集合的一种特定算法。 迭代器对象必须跟踪自身遍历的进度。 这使得多个迭代器可以相互独立地遍历同一集合。
  3. 集合 (Collection) 接口声明一个或多个方法来获取与集合兼容的迭代器。 请注意, 返回方法的类型必须被声明为迭代器接口, 因此具体集合可以返回各种不同种类的迭代器。
  4. 具体集合 (Concrete Collections) 会在客户端请求迭代器时返回一个特定的具体迭代器类实体。 你可能会琢磨, 剩下的集合代码在什么地方呢? 不用担心, 它也会在同一个类中。 只是这些细节对于实际模式来说并不重要, 所以我们将其省略了而已。
  5. 客户端 (Client) 通过集合和迭代器的接口与两者进行交互。 这样一来客户端无需与具体类进行耦合, 允许同一客户端代码使用各种不同的集合和迭代器。
    • 客户端通常不会自行创建迭代器, 而是会从集合中获取。 但在特定情况下, 客户端可以直接创建一个迭代器 (例如当客户端需要自定义特殊迭代器时)。

结构代码范式

Iterator : 定义访问元素的接口。

1
2
3
4
5
6
interface Iterator {
public Object first();
public Object next();
public boolean isDone();
public Object currentItem();
}

ConcreteIterator : 实现 Iterator 接口。记录当前访问的元素在集合中的位置信息。

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
class ConcreteIterator implements Iterator {
private int current = 0;
private ConcreteAggregate aggregate;

public ConcreteIterator(ConcreteAggregate aggregate) {
this.aggregate = aggregate;
}

@Override
public Object first() {
return aggregate.get(0);
}

@Override
public Object next() {
current++;
if (current < aggregate.size()) {
return aggregate.get(current);
}
return null;
}

@Override
public boolean isDone() {
return (current >= aggregate.size()) ? true : false;
}

@Override
public Object currentItem() {
return aggregate.get(current);
}
}

Aggregate : 定义创建 Iterator 对象的接口。

1
2
3
interface Aggregate {
public Iterator CreateIterator();
}

ConcreteAggregate : 实现 Iterator 接口,返回一个合适的 ConcreteIterator 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ConcreteAggregate implements Aggregate {
private List<Object> items = new ArrayList<Object>();

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

public int size() {
return items.size();
}

public Object get(int index) {
return items.get(index);
}

public void set(int index, Object element) {
items.set(index, element);
}

public void add(Object element) {
items.add(element);
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class IteratorPattern {
public static void main(String[] args) {
ConcreteAggregate aggregate = new ConcreteAggregate();
aggregate.add("张三");
aggregate.add("李四");
aggregate.add("王五");
aggregate.add("赵六");

Iterator iter = new ConcreteIterator(aggregate);
Object item = iter.first();
System.out.println("第一个人是:" + item);
System.out.println("所有人的名单是:");
while (!iter.isDone()) {
System.out.println(iter.currentItem());
iter.next();
}
}
}

输出

1
2
3
4
5
6
第一个人是:张三
所有人的名单是:
张三
李四
王五
赵六

伪代码

在本例中, 迭代器模式用于遍历一个封装了访问微信好友关系功能的特殊集合。 该集合提供使用不同方式遍历档案资料的多个迭代器。

img

“好友 (friends)” 迭代器可用于遍历指定档案的好友。 “同事 (colleagues)” 迭代器也提供同样的功能, 但仅包括与目标用户在同一家公司工作的好友。 这两个迭代器都实现了同一个通用接口, 客户端能在不了解认证和发送 REST 请求等实现细节的情况下获取档案。

客户端仅通过接口与集合和迭代器交互, 也就不会同具体类耦合。 如果你决定将应用连接到全新的社交网络, 只需提供新的集合和迭代器类即可, 无需修改现有代码。

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
// 集合接口必须声明一个用于生成迭代器的工厂方法。如果程序中有不同类型的迭
// 代器,你也可以声明多个方法。
interface SocialNetwork is
method createFriendsIterator(profileId):ProfileIterator
method createCoworkersIterator(profileId):ProfileIterator


// 每个具体集合都与其返回的一组具体迭代器相耦合。但客户并不是这样的,因为
// 这些方法的签名将会返回迭代器接口。
class WeChat implements SocialNetwork is
// ...大量的集合代码应该放在这里...

// 迭代器创建代码。
method createFriendsIterator(profileId) is
return new WeChatIterator(this, profileId, "friends")
method createCoworkersIterator(profileId) is
return new WeChatIterator(this, profileId, "coworkers")


// 所有迭代器的通用接口。
interface ProfileIterator is
method getNext():Profile
method hasMore():bool


// 具体迭代器类。
class WeChatIterator implements ProfileIterator is
// 迭代器需要一个指向其遍历集合的引用。
private field weChat: WeChat
private field profileId, type: string

// 迭代器对象会独立于其他迭代器来对集合进行遍历。因此它必须保存迭代器
// 的状态。
private field currentPosition
private field cache: array of Profile

constructor WeChatIterator(weChat, profileId, type) is
this.weChat = weChat
this.profileId = profileId
this.type = type

private method lazyInit() is
if (cache == null)
cache = weChat.socialGraphRequest(profileId, type)

// 每个具体迭代器类都会自行实现通用迭代器接口。
method getNext() is
if (hasMore())
currentPosition++
return cache[currentPosition]

method hasMore() is
lazyInit()
return currentPosition < cache.length


// 这里还有一个有用的绝招:你可将迭代器传递给客户端类,无需让其拥有访问整
// 个集合的权限。这样一来,你就无需将集合暴露给客户端了。
//
// 还有另一个好处:你可在运行时将不同的迭代器传递给客户端,从而改变客户端
// 与集合互动的方式。这一方法可行的原因是客户端代码并没有和具体迭代器类相
// 耦合。
class SocialSpammer is
method send(iterator: ProfileIterator, message: string) is
while (iterator.hasMore())
profile = iterator.getNext()
System.sendEmail(profile.getEmail(), message)


// 应用程序(Application)类可对集合和迭代器进行配置,然后将其传递给客户
// 端代码。
class Application is
field network: SocialNetwork
field spammer: SocialSpammer

method config() is
if working with WeChat
this.network = new WeChat()
if working with LinkedIn
this.network = new LinkedIn()
this.spammer = new SocialSpammer()

method sendSpamToFriends(profile) is
iterator = network.createFriendsIterator(profile.getId())
spammer.send(iterator, "非常重要的消息")

method sendSpamToCoworkers(profile) is
iterator = network.createCoworkersIterator(profile.getId())
spammer.send(iterator, "非常重要的消息")

与其他模式的关系

案例

使用示例: 该模式在 Java 代码中很常见。 许多框架和程序库都会使用它来提供遍历其集合的标准方式。

下面是该模式在核心 Java 程序库中的一些示例:

识别方法: 迭代器可以通过导航方法 (例如 nextprevious等) 来轻松识别。 使用迭代器的客户端代码可能没有其所遍历的集合的直接访问权限。

参考资料

设计模式之外观模式

意图

外观模式 (Facade) 是一种结构型设计模式, 为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

  • 外观模式为复杂子系统提供了一个简单接口,并不为子系统添加新的功能和行为。
  • 外观模式实现了子系统与客户之间的松耦合关系。
  • 外观模式没有封装子系统的类,只是提供了简单的接口。 如果应用需要,它并不限制客户使用子系统类。因此可以再系统易用性与通用性之间选择。
  • 外观模式注重的是简化接口,它更多的时候是从架构的层次去看整个系统,而并非单个类的层次。

适用场景

  • 如果你需要一个指向复杂子系统的直接接口, 且该接口的功能有限, 则可以使用外观模式。
  • 如果需要将子系统组织为多层结构, 可以使用外观。

结构

img

结构说明

  1. 外观 (Facade) 提供了一种访问特定子系统功能的便捷方式, 其了解如何重定向客户端请求, 知晓如何操作一切活动部件。

  2. 创建附加外观 (Additional Facade) 类可以避免多种不相关的功能污染单一外观, 使其变成又一个复杂结构。 客户端和其他外观都可使用附加外观。

  3. 复杂子系统 (Complex Subsystem) 由数十个不同对象构成。 如果要用这些对象完成有意义的工作, 你必须深入了解子系统的实现细节, 比如按照正确顺序初始化对象和为其提供正确格式的数据。

    子系统类不会意识到外观的存在, 它们在系统内运作并且相互之间可直接进行交互。

  4. 客户端 (Client) 使用外观代替对子系统对象的直接调用。

结构代码范式

Facade : 了解每个子系统类的功能,负责分发客户端的请求给各个子系统去处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Class1 {
public void op1() {
System.out.println("方法1");
}
}

class Class2 {
public void op2() {
System.out.println("方法2");
}
}

class Class3 {
public void op3() {
System.out.println("方法3");
}
}

Subsystem Classes : 实现子系统功能。在不感知 Facade 的情况下,处理 Facade 对象分配的工作,

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
class Facade {
private Class1 one = new Class1();
private Class2 two = new Class2();
private Class3 three = new Class3();

public void op1() {
System.out.println("Facade op1()");
one.op1();
}

public void op2() {
System.out.println("Facade op2()");
two.op2();
}

public void op3() {
System.out.println("Facade op3()");
three.op3();
}

public void Method() {
System.out.println("Facade Method()");
three.op3();
two.op2();
one.op1();
}
}

【客户端】

1
2
3
4
5
6
7
8
public class FacadePattern {
public static void main(String[] args) {
Facade facade = new Facade();
facade.Method();

facade.op1();
}
}

【输出】

1
2
3
4
5
6
Facade Method()
方法3
方法2
方法1
Facade op1()
方法1

伪代码

在本例中, 外观模式简化了客户端与复杂视频转换框架之间的交互。

img

你可以创建一个封装所需功能并隐藏其他代码的外观类, 从而无需使全部代码直接与数十个框架类进行交互。 该结构还能将未来框架升级或更换所造成的影响最小化, 因为你只需修改程序中外观方法的实现即可。

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
// 这里有复杂第三方视频转换框架中的一些类。我们不知晓其中的代码,因此无法
// 对其进行简化。

class VideoFile
// ...

class OggCompressionCodec
// ...

class MPEG4CompressionCodec
// ...

class CodecFactory
// ...

class BitrateReader
// ...

class AudioMixer
// ...


// 为了将框架的复杂性隐藏在一个简单接口背后,我们创建了一个外观类。它是在
// 功能性和简洁性之间做出的权衡。
class VideoConverter is
method convert(filename, format):File is
file = new VideoFile(filename)
sourceCodec = new CodecFactory.extract(file)
if (format == "mp4")
destinationCodec = new MPEG4CompressionCodec()
else
destinationCodec = new OggCompressionCodec()
buffer = BitrateReader.read(filename, sourceCodec)
result = BitrateReader.convert(buffer, destinationCodec)
result = (new AudioMixer()).fix(result)
return new File(result)

// 应用程序的类并不依赖于复杂框架中成千上万的类。同样,如果你决定更换框架,
// 那只需重写外观类即可。
class Application is
method main() is
convertor = new VideoConverter()
mp4 = convertor.convert("funny-cats-video.ogg", "mp4")
mp4.save()

案例

使用示例: 使用 Java 开发的程序中经常会使用外观模式。 它在与复杂程序库和 API 协作时特别有用。

下面是一些核心 Java 程序库中的外观示例:

识别方法: 外观可以通过使用简单接口, 但将绝大部分工作委派给其他类的类来识别。 通常情况下, 外观管理着其所使用的对象的完整生命周期。

与其他模式的关系

  • 外观模式为现有对象定义了一个新接口, 适配器模式则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。
  • 当只需对客户端代码隐藏子系统创建对象的方式时, 你可以使用抽象工厂模式来代替外观
  • 享元模式展示了如何生成大量的小型对象, 外观则展示了如何用一个对象来代表整个子系统。
  • 外观中介者模式的职责类似: 它们都尝试在大量紧密耦合的类中组织起合作。
    • 外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。
    • 中介者将系统中组件的沟通行为中心化。 各组件只知道中介者对象, 无法直接相互交流。
  • 外观类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
  • 外观代理模式的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。

参考资料

设计模式之代理模式

意图

代理模式 (Proxy) 是一种结构型设计模式, 为其他对象提供一种代理以控制对这个对象的访问

  • 代理模式介绍了一种访问对象的间接等级。
  • 一个远程代理可以隐藏一个对象在不同地址空间的细节。
  • 一个虚拟代理可以根据需要最优化创建对象的开销。
  • 而安全代理和智能指引都允许访问对象的同时处理其他事务。

适用场景

  • 延迟初始化 (虚拟代理)。 如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。
  • 访问控制 (保护代理)。 如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。
  • 本地执行远程服务 (远程代理)。 适用于服务对象位于远程服务器上的情形。
  • 记录日志请求 (日志记录代理)。 适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。
  • 智能引用。 可在没有客户端使用某个重量级对象时立即销毁该对象。

结构

img

结构说明

  1. 服务接口 (Service Interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。
  2. 服务 (Service) 类提供了一些实用的业务逻辑。
  3. 代理 (Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。
  4. 客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。

结构代码范式

Subject : 定义了 RealSubject 和 Proxy 的公共接口,这样就在任何使用 RealSubject 的地方都可以使用 Proxy 。

1
2
3
abstract class Subject {
public abstract void Request();
}

RealSubject : 定义 Proxy 所代表的真实实体。

1
2
3
4
5
6
class RealSubject extends Subject {
@Override
public void Request() {
System.out.println("真实的请求");
}
}

Proxy : 保存一个引用使得代理可以访问实体,并提供一个与 Subject 的接口相同的接口,这样代理就可以用来替代实体。

1
2
3
4
5
6
7
8
9
10
11
class Proxy extends Subject {
private RealSubject real;

@Override
public void Request() {
if (null == real) {
real = new RealSubject();
}
real.Request();
}
}

伪代码

本例演示如何使用代理模式在第三方腾讯视频 (TencentVideo, 代码示例中记为 TV) 程序库中添加延迟初始化和缓存。

img

程序库提供了视频下载类。 但是该类的效率非常低。 如果客户端程序多次请求同一视频, 程序库会反复下载该视频, 而不会将首次下载的文件缓存下来复用。

代理类实现和原下载器相同的接口, 并将所有工作委派给原下载器。 不过, 代理类会保存所有的文件下载记录, 如果程序多次请求同一文件, 它会返回缓存的文件。

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
// 远程服务接口。
interface ThirdPartyTVLib is
method listVideos()
method getVideoInfo(id)
method downloadVideo(id)

// 服务连接器的具体实现。该类的方法可以向腾讯视频请求信息。请求速度取决于
// 用户和腾讯视频的互联网连接情况。如果同时发送大量请求,即使所请求的信息
// 一模一样,程序的速度依然会减慢。
class ThirdPartyTVClass implements ThirdPartyTVLib is
method listVideos() is
// 向腾讯视频发送一个 API 请求。

method getVideoInfo(id) is
// 获取某个视频的元数据。

method downloadVideo(id) is
// 从腾讯视频下载一个视频文件。

// 为了节省网络带宽,我们可以将请求结果缓存下来并保存一段时间。但你可能无
// 法直接将这些代码放入服务类中。比如该类可能是第三方程序库的一部分或其签
// 名是`final(最终)`。因此我们会在一个实现了服务类接口的新代理类中放入
// 缓存代码。当代理类接收到真实请求后,才会将其委派给服务对象。
class CachedTVClass implements ThirdPartyTVLib is
private field service: ThirdPartyTVLib
private field listCache, videoCache
field needReset

constructor CachedTVClass(service: ThirdPartyTVLib) is
this.service = service

method listVideos() is
if (listCache == null || needReset)
listCache = service.listVideos()
return listCache

method getVideoInfo(id) is
if (videoCache == null || needReset)
videoCache = service.getVideoInfo(id)
return videoCache

method downloadVideo(id) is
if (!downloadExists(id) || needReset)
service.downloadVideo(id)

// 之前直接与服务对象交互的 GUI 类不需要改变,前提是它仅通过接口与服务对
// 象交互。我们可以安全地传递一个代理对象来代替真实服务对象,因为它们都实
// 现了相同的接口。
class TVManager is
protected field service: ThirdPartyTVLib

constructor TVManager(service: ThirdPartyTVLib) is
this.service = service

method renderVideoPage(id) is
info = service.getVideoInfo(id)
// 渲染视频页面。

method renderListPanel() is
list = service.listVideos()
// 渲染视频缩略图列表。

method reactOnUserInput() is
renderVideoPage()
renderListPanel()

// 程序可在运行时对代理进行配置。
class Application is
method init() is
aTVService = new ThirdPartyTVClass()
aTVProxy = new CachedTVClass(aTVService)
manager = new TVManager(aTVProxy)
manager.reactOnUserInput()

案例

使用示例: 尽管代理模式在绝大多数 Java 程序中并不常见, 但它在一些特殊情况下仍然非常方便。 当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时, 该模式是无可替代的。

Java 标准程序库中的一些代理模式的示例:

识别方法: 代理模式会将所有实际工作委派给一些其他对象。 除非代理是某个服务的子类, 否则每个代理方法最后都应该引用一个服务对象。

注解+反射+代理消除重复代码

假设银行提供了一些 API 接口,对参数的序列化有点特殊,不使用 JSON,而是需要我们把参数依次拼在一起构成一个大字符串。

按照银行提供的 API 文档的顺序,把所有参数构成定长的数据,然后拼接在一起作为整个字符串。因为每一种参数都有固定长度,未达到长度时需要做填充处理:

  • 字符串类型的参数不满长度部分需要以下划线右填充,也就是字符串内容靠左;
  • 数字类型的参数不满长度部分以 0 左填充,也就是实际数字靠右;
  • 货币类型的表示需要把金额向下舍入 2 位到分,以分为单位,作为数字类型同样进行
    左填充。

对所有参数做 MD5 操作作为签名(为了方便理解,Demo 中不涉及加盐处理)。

问题版本

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
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.client.fluent.Request;
import org.apache.http.entity.ContentType;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;

public class BankService {

public static String createUser(String name, String identity, String mobile, int age) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
//字符串靠左,多余的地方_填充
stringBuilder.append(String.format("%-10s", name).replace(' ', '_'));
//字符串靠左,多余的地方_填充
stringBuilder.append(String.format("%-18s", identity).replace(' ', '_'));
//数字靠右,多余的地方用0填充
stringBuilder.append(String.format("%05d", age));
//字符串靠左,多余的地方_填充
stringBuilder.append(String.format("%-11s", mobile).replace(' ', '_'));
//最后加上MD5作为签名
stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
return Request.Post("http://localhost:45678/reflection/bank/createUser")
.bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
}

public static String pay(long userId, BigDecimal amount) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
//数字靠右,多余的地方用0填充
stringBuilder.append(String.format("%020d", userId));
//金额向下舍入2位到分,以分为单位,作为数字靠右,多余的地方用0填充
stringBuilder.append(String.format("%010d", amount.setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue()));
//最后加上MD5作为签名
stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
return Request.Post("http://localhost:45678/reflection/bank/pay")
.bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
}
}

在以上的代码版本中,存在以下问题:

  • 三种标准数据类型的处理逻辑有重复,稍有不慎就会出现 Bug;
  • 处理流程中字符串拼接、加签和发请求的逻辑,在所有方法重复;
  • 实际方法的入参的参数类型和顺序,不一定和接口要求一致,容易出错;
  • 代码层面针对每一个参数硬编码,无法清晰地进行核对,如果参数达到几十个、上百个,出错的概率极大。

优化版本

针对上面代码版本中的问题,可以使用 注解+反射+代理模式 解决重复代码。

【注解一】

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface BankAPI {

String desc() default "";

String url() default "";

}

【注解二】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface BankAPIField {

int order() default -1;

int length() default -1;

String type() default "";

}

【抽象类】

1
abstract class AbstractAPI {}

【代理类】

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
@Slf4j
public class BetterBankService {

public static String createUser(String name, String identity, String mobile, int age) throws IOException {
CreateUserAPI createUserAPI = new CreateUserAPI();
createUserAPI.setName(name);
createUserAPI.setIdentity(identity);
createUserAPI.setAge(age);
createUserAPI.setMobile(mobile);
return remoteCall(createUserAPI);
}

public static String pay(long userId, BigDecimal amount) throws IOException {
PayAPI payAPI = new PayAPI();
payAPI.setUserId(userId);
payAPI.setAmount(amount);
return remoteCall(payAPI);
}

private static String remoteCall(AbstractAPI api) throws IOException {
//从BankAPI注解获取请求地址
BankAPI bankAPI = api.getClass().getAnnotation(BankAPI.class);
bankAPI.url();
StringBuilder stringBuilder = new StringBuilder();
Arrays.stream(api.getClass().getDeclaredFields()) //获得所有字段
.filter(field -> field.isAnnotationPresent(BankAPIField.class)) //查找标记了注解的字段
.sorted(Comparator.comparingInt(a -> a.getAnnotation(BankAPIField.class).order())) //根据注解中的order对字段排序
.peek(field -> field.setAccessible(true)) //设置可以访问私有字段
.forEach(field -> {
//获得注解
BankAPIField bankAPIField = field.getAnnotation(BankAPIField.class);
Object value = "";
try {
//反射获取字段值
value = field.get(api);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
//根据字段类型以正确的填充方式格式化字符串
switch (bankAPIField.type()) {
case "S": {
stringBuilder.append(
String.format("%-" + bankAPIField.length() + "s", value.toString()).replace(' ', '_'));
break;
}
case "N": {
stringBuilder.append(
String.format("%" + bankAPIField.length() + "s", value.toString()).replace(' ', '0'));
break;
}
case "M": {
if (!(value instanceof BigDecimal)) {
throw new RuntimeException(
String.format("{} 的 {} 必须是BigDecimal", api, field));
}
stringBuilder.append(String.format("%0" + bankAPIField.length() + "d",
((BigDecimal) value).setScale(2, RoundingMode.DOWN)
.multiply(new BigDecimal("100"))
.longValue()));
break;
}
default:
break;
}
});
//签名逻辑
stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
String param = stringBuilder.toString();
long begin = System.currentTimeMillis();
//发请求
String result = Request.Post("http://localhost:45678/reflection" + bankAPI.url())
.bodyString(param, ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
log.info("调用银行API {} url:{} 参数:{} 耗时:{}ms", bankAPI.desc(), bankAPI.url(), param,
System.currentTimeMillis() - begin);
return result;
}

}

【注解修饰的 API 接口一】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import lombok.Data;

@BankAPI(url = "/bank/createUser", desc = "创建用户接口")
@Data
public class CreateUserAPI extends AbstractAPI {

@BankAPIField(order = 1, type = "S", length = 10)
private String name;
@BankAPIField(order = 2, type = "S", length = 18)
private String identity;
@BankAPIField(order = 4, type = "S", length = 11)
private String mobile;
@BankAPIField(order = 3, type = "N", length = 5)
private int age;

}

【注解修饰的 API 接口二】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import lombok.Data;

import java.math.BigDecimal;

@BankAPI(url = "/bank/pay", desc = "支付接口")
@Data
public class PayAPI extends AbstractAPI {

@BankAPIField(order = 1, type = "N", length = 20)
private long userId;
@BankAPIField(order = 2, type = "M", length = 10)
private BigDecimal amount;

}

与其他模式的关系

  • 适配器模式能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰模式则能为对象提供加强的接口。
  • 外观模式代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。
  • 装饰代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。

参考资料

设计模式之享元模式

意图

享元模式 (Flyweight) 是一种结构型设计模式,它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

适用场景

  • 仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。

结构

img

结构说明

  1. 享元模式只是一种优化。 在应用该模式之前, 你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题, 并且确保该问题无法使用其他更好的方式来解决。
  2. 享元 (Flyweight) 类包含原始对象中部分能在多个对象中共享的状态。 同一享元对象可在许多不同情景中使用。 享元中存储的状态被称为 “内在状态”。 传递给享元方法的状态被称为 “外在状态”。
  3. 情景 (Context) 类包含原始对象中各不相同的外在状态。 情景与享元对象组合在一起就能表示原始对象的全部状态。
  4. 通常情况下, 原始对象的行为会保留在享元类中。 因此调用享元方法必须提供部分外在状态作为参数。 但你也可将行为移动到情景类中, 然后将连入的享元作为单纯的数据对象。
  5. 客户端 (Client) 负责计算或存储享元的外在状态。 在客户端看来, 享元是一种可在运行时进行配置的模板对象, 具体的配置方式为向其方法中传入一些情景数据参数。
  6. 享元工厂 (Flyweight Factory) 会对已有享元的缓存池进行管理。 有了工厂后, 客户端就无需直接创建享元, 它们只需调用工厂并向其传递目标享元的一些内在状态即可。 工厂会根据参数在之前已创建的享元中进行查找, 如果找到满足条件的享元就将其返回; 如果没有找到就根据参数新建享元。

结构代码范式

Flyweight : 它是所有具体享元类的超类或接口,通过这个接口,Flyweight 可以接受并作用于外部状态。

1
2
3
abstract class Flyweight {
public abstract void operation(int extrinsicstates);
}

ConcreteFlyweight : 是继承 Flyweight 超类或实现 Flyweight 接口,并为内部状态增加存储空间。

1
2
3
4
5
6
class ConcreteFlyweight extends Flyweight {
@Override
public void operation(int extrinsicstates) {
System.out.println("共享的Flyweight : " + extrinsicstates);
}
}

UnsharedConcreteFlyweight : 指那些不需要共享的 Flyweight 子类,因为 Flyweight 接口共享成为可能,但它并不强制共享。

1
2
3
4
5
6
class UnsharedConcreteFlyweight extends Flyweight {
@Override
public void operation(int extrinsicstates) {
System.out.println("不共享的Flyweight : " + extrinsicstates);
}
}

FlywightFactory :是一个享元工厂,用来创建并管理 Flyweight 对象。它主要是用来确保合理地共享 Flyweight ,当用户请求一个 Flyweight 时, FlyweightFactory 对象提供一个已创建的实例或创建一个(如果对象不存在的话)。

1
2
3
4
5
6
7
8
9
10
11
12
13
class FlywightFactory {
private Hashtable<String, Flyweight> flyweights = new Hashtable<String, Flyweight>();

public FlywightFactory() {
flyweights.put("X", new ConcreteFlyweight());
flyweights.put("Y", new ConcreteFlyweight());
flyweights.put("Z", new ConcreteFlyweight());
}

public Flyweight getFlyweight(String key) {
return ((Flyweight)flyweights.get(key));
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FlyweightPattern {
public static void main(String[] args) {
int extrinsicstates = 1;
FlywightFactory factory = new FlywightFactory();

Flyweight fx = factory.getFlyweight("X");
fx.operation(extrinsicstates);

Flyweight fy = factory.getFlyweight("Y");
fy.operation(++extrinsicstates);

Flyweight fz = factory.getFlyweight("Z");
fz.operation(++extrinsicstates);

Flyweight uf = new UnsharedConcreteFlyweight();
uf.operation(++extrinsicstates);
}
}

输出

1
2
3
4
共享的Flyweight : 1
共享的Flyweight : 2
共享的Flyweight : 3
不共享的Flyweight : 4

伪代码

img

在本例中, 享元模式能有效减少在画布上渲染数百万个树状对象时所需的内存。

该模式从主要的 Tree 类中抽取内在状态, 并将其移动到享元类 树种类Tree­Type 之中。

最初程序需要在多个对象中存储相同数据, 而现在仅需在几个享元对象中保存数据, 然后在作为情景的 对象中连入享元即可。 客户端代码使用享元工厂创建树对象并封装搜索指定对象的复杂行为, 并能在需要时复用对象。

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
// 享元类包含一个树的部分状态。这些成员变量保存的数值对于特定树而言是唯一
// 的。例如,你在这里找不到树的坐标。但这里有很多树木之间所共有的纹理和颜
// 色。由于这些数据的体积通常非常大,所以如果让每棵树都其进行保存的话将耗
// 费大量内存。因此,我们可将纹理、颜色和其他重复数据导出到一个单独的对象
// 中,然后让众多的单个树对象去引用它。
class TreeType is
field name
field color
field texture
constructor TreeType(name, color, texture) { ... }
method draw(canvas, x, y) is
// 1. 创建特定类型、颜色和纹理的位图。
// 2. 在画布坐标 (X,Y) 处绘制位图。

// 享元工厂决定是否复用已有享元或者创建一个新的对象。
class TreeFactory is
static field treeTypes: collection of tree types
static method getTreeType(name, color, texture) is
type = treeTypes.find(name, color, texture)
if (type == null)
type = new TreeType(name, color, texture)
treeTypes.add(type)
return type

// 情景对象包含树状态的外在部分。程序中可以创建数十亿个此类对象,因为它们
// 体积很小:仅有两个整型坐标和一个引用成员变量。
class Tree is
field x,y
field type: TreeType
constructor Tree(x, y, type) { ... }
method draw(canvas) is
type.draw(canvas, this.x, this.y)

// 树(Tree)和森林(Forest)类是享元的客户端。如果不打算继续对树类进行开
// 发,你可以将它们合并。
class Forest is
field trees: collection of Trees

method plantTree(x, y, name, color, texture) is
type = TreeFactory.getTreeType(name, color, texture)
tree = new Tree(x, y, type)
trees.add(tree)

method draw(canvas) is
foreach (tree in trees) do
tree.draw(canvas)

案例

使用示例: 享元模式只有一个目的: 将内存消耗最小化。 如果你的程序没有遇到内存容量不足的问题, 则可以暂时忽略该模式。

享元模式在核心 Java 程序库中的示例:

识别方法: 享元可以通过构建方法来识别, 它会返回缓存对象而不是创建新的对象。

与其他模式的关系

  • 你可以使用享元模式实现组合模式树的共享叶节点以节省内存。
  • 享元展示了如何生成大量的小型对象, 外观模式则展示了如何用一个对象来代表整个子系统。
  • 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元就和单例模式类似了。 但这两个模式有两个根本性的不同。
    1. 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。 享元对象是不可变的。

参考资料