装饰器模式
假设我们要渲染一段 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 delTextNode = new DelTextNode(textNode); System.out.println(delTextNode.getText()); } }
|
我们发现每一个效果类对外提供的接口都是一样的,不妨将其设成一个装饰器类:
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()); } }
|
访问者模式
访问者模式是最复杂的设计模式之一,概念十分抽象,我们直接从例子切入:
一家公司有工程师(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; ... } 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; } else if (staff instanceof Engineer) { Engineer engineer = (Engineer) staff; } } } class CTO implements ChiefOfficer { @Override private void visit(Staff staff) { if (staff instanceof Manager) { } else if (staff instanceof Engineer) { } } }
|
这种方法的痛点在于 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); }
public class CTOVisitor implements Visitor { @Override public void visit(Engineer engineer) { } @Override public void visit(Manager manager) { } }
public class CEOVisitor implements Visitor { @Override public void visit(Engineer engineer) { } @Override public void visit(Manager manager) { } }
|
但是这样还不够,我们不希望在客户端直接调用 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; 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 重新渲染页面。
此处有一个重要问题:Controller 如何监听用户对 View 的操作?即如何让用户在点击某个按钮或做出其他操作时,Controller 能做出相应反应?有两种 Naive 的做法:
- View 维护 UI 组件的状态,如用户点击按钮时标记被点击的组件;Controller 定期持续轮询所有 UI 组件,当发现被组件被标记点击时,调用相应处理函数
callbackFunction
。
- View 直接依赖 Controller ,持有 Controller 实例,触发 UI 事件时,View 直接调用对应函数。
这两种做法都有严重问题:
- 做法 1 :Controller 持续轮询 UI 组件性能太差,且如果用户在一个轮询周期内做出两个相互依赖的操作,将导致时序问题。
- 做法 2 :这严重违背 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
| class View { ... }
class AddRecipeView extends View { ...
addHandlerUpload(handler) { this._paretElement.addEventListener("submit", function(e) { ... 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
|
import * as model from "./model.js"; import { addRecipeView } from "view.js";
...
const controlAddRecipe = async function (newRecipe) { try { console.log(newRecipe); addRecipeView.renderSpinner();
await model.uploadRecipe(newRecipe); console.log(model.state.recipe);
recipeView.render(model.state.recipe);
addRecipeView.renderMessage();
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
|
export const uploadRecipe = async function (newRecipe) { ... }
|
责任链模式
责任链模式(Chain of Responsibility)是一种处理请求的模式。它使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。