装饰器模式

假设我们要渲染一段 HTML 文本,HTML 可以标记文本的附加效果,比如一段加粗、加删除线的文本的 HTML 实现可以是:

1
<del><b>Hello World</b></del>

效果: Hello World

当然,现代 Web 前端技术主张使用 CSS 实现这些样式。

实现类似附加效果最 naive 的方法可以是直接写一个类来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface TextNode {
String getText();
}

class BlackNode implements TextNode {
private String text;
public void BlackNode(String text) {
this.text = text;
}
public String getText() {
return "<b>" + text + "</b>";
}
}

public class Main {
public static void main(String[] args) {
TextNode textNode = new BlackNode("Hello World");
System.out.println(textNode.getText());
}
}

如果我们想给黑体文本加上下划线,可以新建一个类:

1
2
3
4
5
6
7
8
9
class DeleteBlackNode implements TextNode {
private String text;
public void DeleteBlackNode(String text) {
this.text = text;
}
public String getText() {
return "<del><b>" + text + "</b></del>";
}
}

但是这样将导致一个严重的问题,一段文本可以添加的效果有粗体、斜体、下划线、删除线、字号、颜色等,这些效果可以任意组合,如果给任意一种组合编写一个类,子类数量会爆炸性增长。

装饰器模式的目的是,一个子类只负责一个效果,通过委托的方式将这种效果累加到已经添加好其他效果的文本上。

为了更好地理解装饰器模式,以上述例子为例,我们希望写出的类是这样的(其中 target 是一个已经添加好部分效果的 TextNode):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DelTextNode implements TextNode {
DelTextNode(TextNode textNode) {
this.target = textNode;
}
public String getText() {
return "<del>" + target.getText() + "</del>";
}
}

public class Main {
public static void main(String[] args) {
TextNode textNode = ...
...
// textNode 是已经添加好一些效果的 TextNode
// 假设是 <b>Hello World</b>
// 现在我们想要往上面添加删除线效果
TextNode delTextNode = new DelTextNode(textNode);
System.out.println(delTextNode.getText());
// 将输出 <del><b>Hello World</b></del>
}
}

我们发现每一个效果类对外提供的接口都是一样的,不妨将其设成一个装饰器类:

1
2
3
4
5
6
7
public abstract class NodeDecorator implements TextNode {
protected final TextNode target;

protected NodeDecorator(TextNode target) {
this.target = target;
}
}

去除所有效果后剩下的是一段文本,这需要一个类实现,我们称为核心类:

1
2
3
4
5
6
7
8
9
10
11
public class TextNodeCore implements TextNode {
private String text;

public void TextNodeCore(String text) {
this.text = text;
}

public String getText() {
return text;
}
}

然后将之前提到的所有效果类修改为 NodeDecorator 装饰器的子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DelTextNode extends NodeDecorator {
DelTextNode(TextNode target) {
super(target)
}
public String getText() {
return "<del>" + target.getText() + "</del>";
}
}
class BlackTextNode extends NodeDecorator {
DelTextNode(TextNode target) {
super(target)
}
public String getText() {
return "<b>" + target.getText() + "</b>";
}
}

要渲染一段加粗、加删除线效果的文本可以这样写:

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
TextNode textNode = new TextNodeCore("Hello World");
TextNode delBlackTextNode =
new DelTextNode(new BlackTextNode(textNode));
System.out.println(delBlackTextNode.getText());
// <del><b>Hello World</b></del>
}
}

访问者模式

访问者模式是最复杂的设计模式之一,概念十分抽象,我们直接从例子切入:

一家公司有工程师(Engineer)和经理(Manager)两种职务,他们实现了 Staff 接口。

工程师的工作指标有代码量和 KPI;经理的工作指标有产品数量和 KPI,假设它们的类如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public abstract class Staff { // 员工

private String name;
private int kpi;// 员工KPI
...
}
public class Engineer extends Staff { // 工程师
private int codeAmount;
...
}
public class Manager extends Staff { // 经理
private int productionAmount;
...
}

年底绩效考核时,公司的 CEO 只关注工程师的代码量和经理的产品数量,CTO 只关注工程师和经理的KPI。

所有工程师和经理都放在一个 List<Staff> 中,CEO 和 CTO 对这些数据的关注点是不一样的。

一种 naive 的做法是:

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
List<Staff> staffList = new ArrayList<>();
...
for(Staff staff : staffList) {
cto.visit(staff);
ceo.visit(staff);
}
}
}
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
interface ChiefOfficer {
void visit(Staff staff);
}
class CEO implements ChiefOfficer {
@Override
private void visit(Staff staff) {
if (staff instanceof Manager) {
Manager manager = (Manager) staff;
// CEO 看经理数据
} else if (staff instanceof Engineer) {
Engineer engineer = (Engineer) staff;
// CEO 看工程师数据
}
}
}
class CTO implements ChiefOfficer {
@Override
private void visit(Staff staff) {
if (staff instanceof Manager) {
// CTO 看经理数据
} else if (staff instanceof Engineer) {
// CTO 看工程师数据
}
}
}

这种方法的痛点在于 if-else 逻辑的嵌套以及类型的强制转换,难以扩展和维护。如果用户类型很多,Chief Officer 也很多,每个人的 visit 方法内的逻辑就会非常复杂。

访问者(Visitor)模式能解决这个问题。其中 visit 指的是对数据的,本例中的 Staff 就是数据。

本例中的 Chief Officer 就是所谓的访问者,我们只需要对其稍加修改:

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
public interface Visitor {
void visit(Engineer engineer); // 访问工程师类型
void visit(Manager manager); // 访问经理类型
}
// CTO访问者
public class CTOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
// CTO 看工程师数据
}
@Override
public void visit(Manager manager) {
// CTO 看经理数据
}
}
// CEO访问者
public class CEOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
// CEO 看工程师数据
}
@Override
public void visit(Manager manager) {
// CEO 看经理数据
}
}

但是这样还不够,我们不希望在客户端直接调用 visit 方法,而是转移到被访问的对象的 accept 方法里使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class Staff {
private String name;
private int kpi;// 员工KPI
public abstract void accept(Visitor visitor);
...
}
public class Engineer extends Staff {
private int codeAmount;
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
...
}
public class Manager extends Staff {
private int productionAmount;
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
...
}

当被访问对象存在特殊的数据结构时,客户端并不关心这些结构,只有被访问者才知道以何种方式调用 visit 方法。也就是说,实际环境中 accept 方法中可能不止 visitor.visit(this); 这样简单的逻辑,可能有其他操作,如果不将调用 visit 方法的责任交给被访问对象,这些逻辑就会写在客户端中,要知道我们可能有很多种被访问对象,届时客户端会变得非常复杂。

观察者模式

在前端领域常用的 MVC 架构模式中,有三个重要的组件:Model、View 和 Controller。图中实线箭头表示模块间的依赖关系,虚线箭头表示数据流向。用户点击 UI 上的某个按钮时,Controller 将相应这个点击事件,调用 Model 更新 UI 状态,然后 Controller 重新渲染页面。

图 2

此处有一个重要问题:Controller 如何监听用户对 View 的操作?即如何让用户在点击某个按钮或做出其他操作时,Controller 能做出相应反应?有两种 Naive 的做法:

  1. View 维护 UI 组件的状态,如用户点击按钮时标记被点击的组件;Controller 定期持续轮询所有 UI 组件,当发现被组件被标记点击时,调用相应处理函数 callbackFunction
  2. View 直接依赖 Controller ,持有 Controller 实例,触发 UI 事件时,View 直接调用对应函数。

这两种做法都有严重问题:

  • 做法 11 :Controller 持续轮询 UI 组件性能太差,且如果用户在一个轮询周期内做出两个相互依赖的操作,将导致时序问题。
  • 做法 22 :这严重违背 MVC 模式的设计初衷,导致 Controller 和 View 循环依赖,相互耦合。

解决方案是使用观察者模式,对于一个 UI 组件,View 只持有 callbackFunction 的引用,而不是整个 Controller ,使用时只需要确定好 callbackFunction 的接口规约,就能实现 Controller 对 View 的单向依赖,即 View 并不知道 Controller 的存在,实现解耦。

以一个 Web 前端菜谱管理页面为例,我们要实现通过一个按钮添加菜谱的功能,点击 Add Recipe 按钮,AddRecipeView 组件将获得用户输入的菜谱数据 data ,我们需要让 Controller 监听这一事件,并让 Controller 将原始 data 传递给 Model 。

View 部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// view.js
class View {
...
// View 基类的实现省略
}

class AddRecipeView extends View {
...
// AddRecipeView 其他部分省略

addHandlerUpload(handler) {
this._paretElement.addEventListener("submit", function(e) {
// 只保留 data 传递的业务逻辑
...
const data = ...
...
handler(data);
});
}
}

export default new AddRecipeView();

Controller 部分

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
// controller.js

import * as model from "./model.js";
import { addRecipeView } from "view.js";

...

const controlAddRecipe = async function (newRecipe) {
// Controller 响应事件,上传、更新数据、重新渲染页面
// 省略部分逻辑
try {
console.log(newRecipe);
// Show loading spinner
addRecipeView.renderSpinner();

// Upload new recipe data
await model.uploadRecipe(newRecipe);
console.log(model.state.recipe);

// Render recipe
recipeView.render(model.state.recipe);

// Succes message
addRecipeView.renderMessage();

// Render bookmarksView
bookmarksView.render(model.state.bookmarks);

...

} catch (err) {
console.error(err);
addRecipeView.renderError(err.message);
}
};


const init = function () {

...

addRecipeView.addHandlerUpload(controlAddRecipe);
};

init();

Model 部分

1
2
3
4
5
6
// model.js

export const uploadRecipe = async function (newRecipe) {
...
// 上传菜谱的逻辑
}

责任链模式

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