行为型模式(下)—— 责任链、命令、状态、中介者、备忘录、访问者
这一章一口气介绍六个行为型模式。它们分别处理不同的协作难题:请求的逐级传递、操作的封装与撤销、状态驱动的行为切换、多对象之间的通信协调、状态的快照与恢复、以及对复杂数据结构的遍历操作。
💡 学习建议:本章信息密度较高,建议分两次阅读——前三个模式(责任链、命令、状态)为一组,后三个模式(中介者、备忘录、访问者)为一组,每组消化后再进入下一组。
📋 开篇自测:你已经知道多少?
- Express/Koa 的中间件机制背后对应的是什么设计模式?
- 文本编辑器的”撤销/重做”功能,通常用什么模式来实现?
- 一个角色在”正常”、“中毒”、“眩晕”三种状态下行为完全不同,你会怎么组织代码?
一、责任链模式:请求沿链条逐级传递
1.1 从表单验证管线说起
假设你在开发一个用户注册表单,提交前需要进行多项验证:检查必填项 → 检查格式合法性 → 检查用户名唯一性 → 检查密码强度。如果任何一项验证失败,就终止流程并返回错误信息。
// 反面教材:所有验证逻辑耦合在一起
function validateRegistration(form: RegistrationForm): string | null {
// 必填检查
if (!form.username) return '用户名不能为空';
if (!form.email) return '邮箱不能为空';
if (!form.password) return '密码不能为空';
// 格式检查
if (!/^[a-zA-Z0-9_]{3,20}$/.test(form.username)) return '用户名格式不正确';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) return '邮箱格式不正确';
// 唯一性检查(需要异步查数据库,但这里是同步函数...)
// 密码强度检查
if (form.password.length < 8) return '密码至少 8 位';
return null;
}
问题:所有验证逻辑混在一起,无法灵活地增删验证步骤。不同的表单(注册、修改资料、管理员创建用户)可能需要不同的验证组合,但现在无法复用单个验证环节。
责任链模式的核心思想是:将请求的发送者和接收者解耦,让多个处理者对象组成一条链,请求沿着链传递,直到被某个处理者处理或到达链末端。
1.2 责任链实现
interface ValidationResult {
valid: boolean;
error?: string;
}
interface RegistrationForm {
username: string;
email: string;
password: string;
}
// 验证处理器抽象类
abstract class FormValidator {
private next: FormValidator | null = null;
setNext(validator: FormValidator): FormValidator {
this.next = validator;
return validator; // 支持链式设置
}
async handle(form: RegistrationForm): Promise<ValidationResult> {
const result = await this.validate(form);
if (!result.valid) {
return result; // 验证失败,终止链条
}
// 验证通过,交给下一个处理者
if (this.next) {
return this.next.handle(form);
}
return { valid: true }; // 所有处理者都通过了
}
protected abstract validate(form: RegistrationForm): Promise<ValidationResult>;
}
// 具体处理者
class RequiredFieldValidator extends FormValidator {
protected async validate(form: RegistrationForm): Promise<ValidationResult> {
if (!form.username?.trim()) return { valid: false, error: '用户名不能为空' };
if (!form.email?.trim()) return { valid: false, error: '邮箱不能为空' };
if (!form.password) return { valid: false, error: '密码不能为空' };
return { valid: true };
}
}
class FormatValidator extends FormValidator {
protected async validate(form: RegistrationForm): Promise<ValidationResult> {
if (!/^[a-zA-Z0-9_]{3,20}$/.test(form.username)) {
return { valid: false, error: '用户名只能包含字母、数字和下划线,长度 3-20' };
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
return { valid: false, error: '邮箱格式不正确' };
}
return { valid: true };
}
}
class PasswordStrengthValidator extends FormValidator {
protected async validate(form: RegistrationForm): Promise<ValidationResult> {
if (form.password.length < 8) {
return { valid: false, error: '密码至少 8 位' };
}
if (!/[A-Z]/.test(form.password) || !/[0-9]/.test(form.password)) {
return { valid: false, error: '密码必须包含大写字母和数字' };
}
return { valid: true };
}
}
class UniqueUsernameValidator extends FormValidator {
private existingUsers = new Set(['admin', 'root', 'test']);
protected async validate(form: RegistrationForm): Promise<ValidationResult> {
// 模拟异步数据库查询
await new Promise(r => setTimeout(r, 100));
if (this.existingUsers.has(form.username.toLowerCase())) {
return { valid: false, error: `用户名 "${form.username}" 已被注册` };
}
return { valid: true };
}
}
组装并使用:
// 组装验证链
const validator = new RequiredFieldValidator();
validator
.setNext(new FormatValidator())
.setNext(new PasswordStrengthValidator())
.setNext(new UniqueUsernameValidator());
// 验证
const result = await validator.handle({
username: 'alice',
email: 'alice@example.com',
password: 'MyPass123',
});
console.log(result); // { valid: true }
const result2 = await validator.handle({
username: 'admin',
email: 'admin@example.com',
password: 'MyPass123',
});
console.log(result2); // { valid: false, error: '用户名 "admin" 已被注册' }
二、命令模式:操作的封装与撤销
2.1 从富文本编辑器说起
假设你在开发一个富文本编辑器,需要支持撤销和重做功能。用户的每一步操作(输入文字、加粗、插入图片、删除段落)都要能回退。
命令模式的核心思想是:将一个请求封装成一个对象,从而使你可以用不同的请求来参数化客户端,支持排队、记录日志、撤销等操作。
2.2 命令模式实现
// 命令接口
interface EditorCommand {
execute(): void;
undo(): void;
getDescription(): string;
}
// 编辑器状态
class TextDocument {
private content: string[] = [];
getContent(): string { return this.content.join(''); }
getLines(): string[] { return [...this.content]; }
insertAt(position: number, text: string): void {
this.content.splice(position, 0, text);
}
removeAt(position: number, count: number): string[] {
return this.content.splice(position, count);
}
replaceAt(position: number, text: string): string {
const old = this.content[position];
this.content[position] = text;
return old;
}
}
// 具体命令:插入文本
class InsertTextCommand implements EditorCommand {
constructor(
private doc: TextDocument,
private position: number,
private text: string
) {}
execute(): void {
this.doc.insertAt(this.position, this.text);
}
undo(): void {
this.doc.removeAt(this.position, 1);
}
getDescription(): string {
return `插入 "${this.text.substring(0, 20)}${this.text.length > 20 ? '...' : ''}"`;
}
}
// 具体命令:删除文本
class DeleteTextCommand implements EditorCommand {
private deletedContent: string[] = [];
constructor(
private doc: TextDocument,
private position: number,
private count: number
) {}
execute(): void {
this.deletedContent = this.doc.removeAt(this.position, this.count);
}
undo(): void {
// 恢复被删除的内容
this.deletedContent.forEach((text, i) => {
this.doc.insertAt(this.position + i, text);
});
}
getDescription(): string {
return `删除 ${this.count} 个字符`;
}
}
// 具体命令:文本加粗
class BoldTextCommand implements EditorCommand {
private previousText = '';
constructor(
private doc: TextDocument,
private position: number
) {}
execute(): void {
const lines = this.doc.getLines();
if (this.position < lines.length) {
this.previousText = lines[this.position];
this.doc.replaceAt(this.position, `<b>${this.previousText}</b>`);
}
}
undo(): void {
this.doc.replaceAt(this.position, this.previousText);
}
getDescription(): string { return '加粗文本'; }
}
// 命令管理器:支持撤销/重做
class CommandHistory {
private history: EditorCommand[] = [];
private undoneStack: EditorCommand[] = [];
executeCommand(command: EditorCommand): void {
command.execute();
this.history.push(command);
this.undoneStack = []; // 新操作清空重做栈
console.log(`执行: ${command.getDescription()}`);
}
undo(): void {
const command = this.history.pop();
if (command) {
command.undo();
this.undoneStack.push(command);
console.log(`撤销: ${command.getDescription()}`);
} else {
console.log('没有可撤销的操作');
}
}
redo(): void {
const command = this.undoneStack.pop();
if (command) {
command.execute();
this.history.push(command);
console.log(`重做: ${command.getDescription()}`);
} else {
console.log('没有可重做的操作');
}
}
}
使用:
const doc = new TextDocument();
const history = new CommandHistory();
history.executeCommand(new InsertTextCommand(doc, 0, 'Hello'));
history.executeCommand(new InsertTextCommand(doc, 1, ' World'));
history.executeCommand(new BoldTextCommand(doc, 0));
console.log(doc.getContent()); // <b>Hello</b> World
history.undo(); // 撤销加粗
console.log(doc.getContent()); // Hello World
history.undo(); // 撤销插入 " World"
console.log(doc.getContent()); // Hello
history.redo(); // 重做插入 " World"
console.log(doc.getContent()); // Hello World
三、状态模式:让对象在不同状态下表现不同
3.1 从工单流转系统说起
一个客服工单有多种状态:待受理、处理中、待评审、已完成、已关闭。不同状态下,同一操作的行为不同——处理中的工单可以提交评审,但已关闭的工单不能再操作。
状态模式的核心思想是:允许一个对象在其内部状态改变时改变它的行为,使对象看起来好像修改了它的类。
3.2 状态模式实现
// 状态接口
interface TicketState {
getName(): string;
assign(ticket: SupportTicket, agent: string): void;
resolve(ticket: SupportTicket, solution: string): void;
review(ticket: SupportTicket, approved: boolean): void;
close(ticket: SupportTicket, reason: string): void;
}
class SupportTicket {
private state: TicketState;
agent: string | null = null;
solution: string | null = null;
constructor(public readonly id: string, public readonly title: string) {
this.state = new PendingState();
}
setState(state: TicketState): void {
console.log(`工单 ${this.id} 状态变更: ${this.state.getName()} → ${state.getName()}`);
this.state = state;
}
getStateName(): string { return this.state.getName(); }
// 委托给当前状态对象
assign(agent: string): void { this.state.assign(this, agent); }
resolve(solution: string): void { this.state.resolve(this, solution); }
review(approved: boolean): void { this.state.review(this, approved); }
close(reason: string): void { this.state.close(this, reason); }
}
// 具体状态:待受理
class PendingState implements TicketState {
getName(): string { return '待受理'; }
assign(ticket: SupportTicket, agent: string): void {
ticket.agent = agent;
ticket.setState(new InProgressState());
console.log(`客服 ${agent} 接手工单`);
}
resolve(): void { console.log('操作无效:工单尚未受理,不能直接解决'); }
review(): void { console.log('操作无效:工单尚未受理'); }
close(ticket: SupportTicket, reason: string): void {
ticket.setState(new ClosedState());
console.log(`工单关闭: ${reason}`);
}
}
// 具体状态:处理中
class InProgressState implements TicketState {
getName(): string { return '处理中'; }
assign(ticket: SupportTicket, agent: string): void {
ticket.agent = agent;
console.log(`工单转交给 ${agent}`);
}
resolve(ticket: SupportTicket, solution: string): void {
ticket.solution = solution;
ticket.setState(new ReviewState());
console.log(`已提交解决方案,等待评审`);
}
review(): void { console.log('操作无效:尚未提交解决方案'); }
close(ticket: SupportTicket, reason: string): void {
ticket.setState(new ClosedState());
console.log(`工单关闭: ${reason}`);
}
}
// 具体状态:待评审
class ReviewState implements TicketState {
getName(): string { return '待评审'; }
assign(): void { console.log('操作无效:评审中不能转交'); }
resolve(): void { console.log('操作无效:已提交方案,请等待评审'); }
review(ticket: SupportTicket, approved: boolean): void {
if (approved) {
ticket.setState(new CompletedState());
console.log('评审通过,工单已完成');
} else {
ticket.setState(new InProgressState());
console.log('评审未通过,退回处理');
}
}
close(ticket: SupportTicket, reason: string): void {
ticket.setState(new ClosedState());
console.log(`工单关闭: ${reason}`);
}
}
// 具体状态:已完成
class CompletedState implements TicketState {
getName(): string { return '已完成'; }
assign(): void { console.log('操作无效:工单已完成'); }
resolve(): void { console.log('操作无效:工单已完成'); }
review(): void { console.log('操作无效:工单已完成'); }
close(ticket: SupportTicket, reason: string): void {
ticket.setState(new ClosedState());
console.log(`工单归档关闭: ${reason}`);
}
}
// 具体状态:已关闭
class ClosedState implements TicketState {
getName(): string { return '已关闭'; }
assign(): void { console.log('操作无效:工单已关闭'); }
resolve(): void { console.log('操作无效:工单已关闭'); }
review(): void { console.log('操作无效:工单已关闭'); }
close(): void { console.log('操作无效:工单已关闭'); }
}
使用:
const ticket = new SupportTicket('TK-001', '页面加载缓慢');
ticket.assign('Alice'); // 客服 Alice 接手
ticket.resolve('优化了数据库查询'); // 提交方案
ticket.review(false); // 评审未通过,退回
ticket.resolve('增加了索引和缓存'); // 重新提交
ticket.review(true); // 评审通过
ticket.close('客户确认问题已修复'); // 归档
四、中介者模式:解耦多对象之间的通信
4.1 从聊天室说起
假设你在开发一个在线聊天室。如果每个用户对象都直接引用其他所有用户来发送消息,当用户数增长时,对象之间的关系会变成一张复杂的网。
中介者模式的核心思想是:用一个中介对象来封装一组对象之间的交互,使这些对象不需要直接引用彼此。
interface ChatMediator {
register(user: ChatUser): void;
sendMessage(message: string, sender: ChatUser, targetRoom: string): void;
sendPrivate(message: string, sender: ChatUser, targetName: string): void;
}
class ChatRoom implements ChatMediator {
private users = new Map<string, ChatUser>();
register(user: ChatUser): void {
this.users.set(user.name, user);
console.log(`${user.name} 加入了聊天室`);
// 通知其他人
this.users.forEach((u) => {
if (u !== user) u.receive(`系统消息: ${user.name} 加入了聊天室`, 'system');
});
}
sendMessage(message: string, sender: ChatUser, targetRoom: string): void {
this.users.forEach((user) => {
if (user !== sender) {
user.receive(`[${targetRoom}] ${sender.name}: ${message}`, sender.name);
}
});
}
sendPrivate(message: string, sender: ChatUser, targetName: string): void {
const target = this.users.get(targetName);
if (target) {
target.receive(`[私聊] ${sender.name}: ${message}`, sender.name);
} else {
sender.receive(`系统消息: 用户 ${targetName} 不在线`, 'system');
}
}
}
class ChatUser {
constructor(
public readonly name: string,
private mediator: ChatMediator
) {
mediator.register(this);
}
send(message: string, room = 'general'): void {
console.log(`${this.name} 发送: ${message}`);
this.mediator.sendMessage(message, this, room);
}
sendPrivateTo(message: string, targetName: string): void {
this.mediator.sendPrivate(message, this, targetName);
}
receive(message: string, from: string): void {
console.log(` ${this.name} 收到 [来自 ${from}]: ${message}`);
}
}
使用:
const chatRoom = new ChatRoom();
const alice = new ChatUser('Alice', chatRoom);
const bob = new ChatUser('Bob', chatRoom);
const charlie = new ChatUser('Charlie', chatRoom);
alice.send('大家好!');
bob.sendPrivateTo('嗨 Alice,周五有空吗?', 'Alice');
每个 ChatUser 只和 ChatRoom 打交道,不需要知道其他用户的存在。
五、备忘录模式:状态的快照与恢复
5.1 从表单草稿箱说起
用户在填写一个复杂的表单时,可能中途想”回退到之前某一步”。备忘录模式可以保存表单在某个时刻的完整状态快照,随时恢复。
备忘录模式的核心思想是:在不暴露对象内部细节的前提下,捕获并保存对象的内部状态,以便以后可以将对象恢复到该状态。
// 备忘录:存储表单快照
class FormSnapshot {
constructor(
private readonly data: Record<string, any>,
public readonly timestamp: Date,
public readonly label: string
) {}
getData(): Record<string, any> {
return JSON.parse(JSON.stringify(this.data)); // 返回深拷贝,防止被外部修改
}
}
// 发起人:复杂表单
class ApplicationForm {
private fields: Record<string, any> = {};
setField(key: string, value: any): void {
this.fields[key] = value;
}
getFields(): Record<string, any> {
return { ...this.fields };
}
// 创建快照
save(label: string): FormSnapshot {
return new FormSnapshot(
JSON.parse(JSON.stringify(this.fields)),
new Date(),
label
);
}
// 从快照恢复
restore(snapshot: FormSnapshot): void {
this.fields = snapshot.getData();
console.log(`表单已恢复到: ${snapshot.label} (${snapshot.timestamp.toLocaleString()})`);
}
}
// 管理者:草稿箱
class DraftManager {
private drafts: FormSnapshot[] = [];
private maxDrafts: number;
constructor(maxDrafts = 10) {
this.maxDrafts = maxDrafts;
}
saveDraft(form: ApplicationForm, label: string): void {
const snapshot = form.save(label);
this.drafts.push(snapshot);
if (this.drafts.length > this.maxDrafts) {
this.drafts.shift(); // 超出限制则丢弃最早的
}
console.log(`草稿已保存: ${label}`);
}
restoreDraft(form: ApplicationForm, index: number): void {
if (index < 0 || index >= this.drafts.length) {
console.log('草稿不存在');
return;
}
form.restore(this.drafts[index]);
}
listDrafts(): string[] {
return this.drafts.map((d, i) => `[${i}] ${d.label} - ${d.timestamp.toLocaleString()}`);
}
}
注意事项:备忘录模式的主要陷阱是内存开销。每次保存快照都是一次完整的状态复制,如果状态对象很大(比如包含大量嵌套数据或二进制内容),频繁保存会导致内存快速增长。上面的 DraftManager 通过 maxDrafts 限制了快照数量,这是最基本的防护手段。更高级的做法包括:只保存变化的增量(delta)而非完整快照、将旧快照序列化到磁盘或 IndexedDB 中、以及采用写时复制(Copy-on-Write)策略来延迟深拷贝的开销。
使用:
const form = new ApplicationForm();
const drafts = new DraftManager();
form.setField('name', '张三');
form.setField('age', 28);
drafts.saveDraft(form, '填写个人信息');
form.setField('education', '本科');
form.setField('school', '清华大学');
drafts.saveDraft(form, '填写教育经历');
form.setField('school', '北京大学'); // 改错了,想回退
console.log(drafts.listDrafts());
drafts.restoreDraft(form, 0); // 恢复到"填写个人信息"
console.log(form.getFields()); // { name: '张三', age: 28 }
六、访问者模式:为数据结构增加新操作
6.1 从代码分析工具说起
假设你在开发一个 TypeScript AST(抽象语法树)分析工具。AST 的节点类型是相对稳定的(函数声明、变量声明、表达式等),但你需要频繁增加新的分析操作(统计行数、检查命名规范、提取依赖关系等)。
访问者模式的核心思想是:将作用于数据结构中各元素的操作,从元素类中分离出来,封装成独立的”访问者”对象。 适用于数据结构稳定、但操作频繁变化的场景。
// AST 节点接口
interface ASTNode {
accept(visitor: ASTVisitor): void;
}
// 访问者接口
interface ASTVisitor {
visitFunction(node: FunctionNode): void;
visitVariable(node: VariableNode): void;
visitImport(node: ImportNode): void;
}
// 具体节点
class FunctionNode implements ASTNode {
constructor(
public name: string,
public params: string[],
public lineCount: number
) {}
accept(visitor: ASTVisitor): void {
visitor.visitFunction(this);
}
}
class VariableNode implements ASTNode {
constructor(
public name: string,
public kind: 'const' | 'let' | 'var',
public hasTypeAnnotation: boolean
) {}
accept(visitor: ASTVisitor): void {
visitor.visitVariable(this);
}
}
class ImportNode implements ASTNode {
constructor(
public source: string,
public specifiers: string[]
) {}
accept(visitor: ASTVisitor): void {
visitor.visitImport(this);
}
}
// 具体访问者:代码复杂度分析
class ComplexityAnalyzer implements ASTVisitor {
private longFunctions: string[] = [];
private totalFunctions = 0;
visitFunction(node: FunctionNode): void {
this.totalFunctions++;
if (node.lineCount > 30) {
this.longFunctions.push(`${node.name} (${node.lineCount}行)`);
}
}
visitVariable(_node: VariableNode): void { /* 复杂度分析不关心变量 */ }
visitImport(_node: ImportNode): void { /* 也不关心导入 */ }
getReport(): string {
return `函数总数: ${this.totalFunctions},超长函数: ${this.longFunctions.join(', ') || '无'}`;
}
}
// 具体访问者:命名规范检查
class NamingConventionChecker implements ASTVisitor {
private violations: string[] = [];
visitFunction(node: FunctionNode): void {
if (!/^[a-z][a-zA-Z0-9]*$/.test(node.name)) {
this.violations.push(`函数 "${node.name}" 应使用 camelCase`);
}
}
visitVariable(node: VariableNode): void {
const isUpperCaseConst = node.kind === 'const' && node.name === node.name.toUpperCase();
const isCamelCase = /^[a-z][a-zA-Z0-9]*$/.test(node.name);
// 允许两种命名:camelCase 变量名,或全大写的 const 常量(如 MAX_RETRY)
if (!isCamelCase && !isUpperCaseConst) {
this.violations.push(`变量 "${node.name}" 命名不规范`);
}
}
visitImport(_node: ImportNode): void { /* 导入名由外部库决定,不检查 */ }
getViolations(): string[] { return this.violations; }
}
使用:
// 模拟 AST
const ast: ASTNode[] = [
new ImportNode('lodash', ['debounce', 'throttle']),
new VariableNode('MAX_RETRY', 'const', true),
new FunctionNode('fetchUserData', ['userId'], 15),
new FunctionNode('ProcessOrder', ['order'], 45), // 命名违规 + 超长
new VariableNode('temp_value', 'let', false), // 命名违规
];
// 运行复杂度分析
const complexity = new ComplexityAnalyzer();
ast.forEach(node => node.accept(complexity));
console.log(complexity.getReport());
// 函数总数: 2,超长函数: ProcessOrder (45行)
// 运行命名检查
const naming = new NamingConventionChecker();
ast.forEach(node => node.accept(naming));
console.log(naming.getViolations());
// ['函数 "ProcessOrder" 应使用 camelCase', '变量 "temp_value" 命名不规范']
🤔 想一想 访问者模式被称为”最复杂的设计模式”之一。在现代 TypeScript 中,使用联合类型 + switch-case 的”标签联合”方案能否替代访问者模式?各有什么优劣?
命令模式和备忘录模式都能实现”撤销”功能,它们的区别是什么?提示:命令记录的是”操作”,备忘录记录的是”状态”。
七、本章小结
本章学习了行为型模式的后六位成员:
| 模式 | 核心问题 | 一句话总结 |
|---|---|---|
| 责任链 | 请求需要逐级传递和处理 | 把处理者串成链,请求沿链流转 |
| 命令 | 操作需要封装、排队、撤销 | 把操作变成对象,支持撤销和重做 |
| 状态 | 行为随状态变化而变化 | 每种状态是一个类,委托给当前状态 |
| 中介者 | 多个对象之间互相通信太混乱 | 引入中央协调者,解耦直接引用 |
| 备忘录 | 需要保存和恢复对象状态 | 拍快照存起来,随时恢复 |
| 访问者 | 数据结构稳定但操作频繁变化 | 把操作抽成独立访问者,遍历时调用 |
至此,GoF 的 23 种经典设计模式中的 22 种已经介绍完毕。还有一种——解释器模式(Interpreter Pattern)——因为在前端开发中使用频率较低,这里做一个简要补充。
补充:解释器模式简介
解释器模式为一种语言定义其语法表示,并提供一个解释器来处理该语法。它的典型应用场景包括:模板引擎解析(如 Mustache 的 {{variable}} 语法)、DSL(领域特定语言)执行、正则表达式引擎、数学表达式计算器等。
在前端领域,你可能不会直接手写一个解释器模式的类层级结构,但你每天都在使用基于它思想的工具——Vue 的模板编译器、Babel 的 AST 转换、ESLint 的规则引擎,底层都涉及”定义语法 → 解析 → 解释执行”的过程。如果你对编译原理或 DSL 设计感兴趣,推荐深入学习这个模式。
下一章我们进入实战篇,看看这些模式在现代前端开发中是如何落地的。
📝 结尾自测
- 责任链模式中,处理者之间是什么关系?请求是如何从一个处理者传递到下一个的?
- 命令模式实现”撤销”功能的关键是什么?一个命令对象至少需要哪两个方法?
- 状态模式和 if-else 状态判断相比,优势在哪里?什么时候用状态模式是过度设计?
- 中介者模式解决的核心问题是什么?它有什么潜在的缺点?
- 备忘录模式的”三角色”分别是什么?为什么要保证外部不能修改备忘录的内容?
购买课程解锁全部内容
写出优雅代码:10 章掌握 23 种设计模式
¥29.90