装饰器模式
假设我们要渲染一段 HTML 文本,HTML 可以标记文本的附加效果,比如一段加粗、加删除线的文本的 HTML 实现可以是:
<del><b>Hello World</b></del>
效果: Hello World
当然,现代 Web 前端技术主张使用 CSS 实现这些样式。
实现类似附加效果最 naive 的方法可以是直接写一个类来实现:
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());
}
}
如果我们想给黑体文本加上下划线,可以新建一个类:
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
):
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>
}
}
我们发现每一个效果类对外提供的接口都是一样的,不妨将其设成一个装饰器类:
public abstract class NodeDecorator implements TextNode {
protected final TextNode target;
protected NodeDecorator(TextNode target) {
this.target = target;
}
}
去除所有效果后剩下的是一段文本,这需要一个类实现,我们称为核心类:
public class TextNodeCore implements TextNode {
private String text;
public void TextNodeCore(String text) {
this.text = text;
}
public String getText() {
return text;
}
}
然后将之前提到的所有效果类修改为 NodeDecorator
装饰器的子类:
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>";
}
}
要渲染一段加粗、加删除线效果的文本可以这样写:
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,假设它们的类如下所示:
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 的做法是:
public class Main {
public static void main(String[] args) {
List<Staff> staffList = new ArrayList<>();
...
for(Staff staff : staffList) {
cto.visit(staff);
ceo.visit(staff);
}
}
}
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 就是所谓的访问者,我们只需要对其稍加修改:
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
方法里使用:
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 重新渲染页面。
此处有一个重要问题:Controller 如何监听用户对 View 的操作?即如何让用户在点击某个按钮或做出其他操作时,Controller 能做出相应反应?有两种 Naive 的做法:
- View 维护 UI 组件的状态,如用户点击按钮时标记被点击的组件;Controller 定期持续轮询所有 UI 组件,当发现被组件被标记点击时,调用相应处理函数
callbackFunction
。 - View 直接依赖 Controller ,持有 Controller 实例,触发 UI 事件时,View 直接调用对应函数。
这两种做法都有严重问题:
- 做法 :Controller 持续轮询 UI 组件性能太差,且如果用户在一个轮询周期内做出两个相互依赖的操作,将导致时序问题。
- 做法 :这严重违背 MVC 模式的设计初衷,导致 Controller 和 View 循环依赖,相互耦合。
解决方案是使用观察者模式,对于一个 UI 组件,View 只持有 callbackFunction
的引用,而不是整个 Controller ,使用时只需要确定好 callbackFunction
的接口规约,就能实现 Controller 对 View 的单向依赖,即 View 并不知道 Controller 的存在,实现解耦。
以一个 Web 前端菜谱管理页面为例,我们要实现通过一个按钮添加菜谱的功能,点击 Add Recipe
按钮,AddRecipeView
组件将获得用户输入的菜谱数据 data
,我们需要让 Controller 监听这一事件,并让 Controller 将原始 data
传递给 Model 。
View 部分:
// 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 部分
// 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 部分
// model.js
export const uploadRecipe = async function (newRecipe) {
...
// 上传菜谱的逻辑
}
责任链模式
责任链模式(Chain of Responsibility)是一种处理请求的模式。它使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。