下面是一个完整的、严格模式下的 TypeScript 实现,包含 CommandManager、Command 和 CompositeCommand 类,支持 undo/redo 功能。
完整实现代码
1. Command 接口和基类
TypeScript
// src/commands/ICommand.ts
export interface ICommand {
execute(): void;
undo(): void;
redo(): void;
}
TypeScript
// src/commands/Command.ts
import type{ ICommand } from "./ICommand";
export abstract class Command implements ICommand {
private _isExecuted: boolean = false;
public execute(): void {
if (!this._isExecuted) {
this.doExecute();
this._isExecuted = true;
}
}
public undo(): void {
if (this._isExecuted) {
this.doUndo();
this._isExecuted = false;
}
}
public redo(): void {
if (!this._isExecuted) {
this.doExecute();
this._isExecuted = true;
}
}
protected abstract doExecute(): void;
protected abstract doUndo(): void;
public get isExecuted(): boolean {
return this._isExecuted;
}
}
2. CompositeCommand 实现
TypeScript
// src/commands/CompositeCommand.ts
import type{ ICommand } from "./ICommand";
import { Command } from "./Command";
export class CompositeCommand extends Command {
private readonly _commands: ICommand[] = [];
public addCommand(command: ICommand): void {
this._commands.push(command);
}
public removeCommand(command: ICommand): void {
const index = this._commands.indexOf(command);
if (index !== -1) {
this._commands.splice(index, 1);
}
}
public clearCommands(): void {
this._commands.length = 0;
}
public get commands(): ReadonlyArray<ICommand> {
return this._commands;
}
protected doExecute(): void {
for (const command of this._commands) {
command.execute();
}
}
protected doUndo(): void {
// Undo in reverse order
for (let i = this._commands.length - 1; i >= 0; i--) {
this._commands[i].undo();
}
}
}
3. CommandManager 实现
TypeScript
// src/commands/CommandManager.ts
import type{ ICommand } from "./ICommand";
export class CommandManager {
private readonly _undoStack: ICommand[] = [];
private readonly _redoStack: ICommand[] = [];
private readonly _maxStackSize: number;
constructor(maxStackSize: number = 100) {
this._maxStackSize = maxStackSize;
}
public execute(command: ICommand): void {
command.execute();
this._undoStack.push(command);
// Clear redo stack when executing a new command
this._redoStack.length = 0;
// Ensure we don't exceed max stack size
if (this._undoStack.length > this._maxStackSize) {
this._undoStack.shift();
}
}
public undo(): boolean {
const command = this._undoStack.pop();
if (command) {
command.undo();
this._redoStack.push(command);
return true;
}
return false;
}
public redo(): boolean {
const command = this._redoStack.pop();
if (command) {
command.redo();
this._undoStack.push(command);
return true;
}
return false;
}
public clearHistory(): void {
this._undoStack.length = 0;
this._redoStack.length = 0;
}
public get canUndo(): boolean {
return this._undoStack.length > 0;
}
public get canRedo(): boolean {
return this._redoStack.length > 0;
}
public get undoStackSize(): number {
return this._undoStack.length;
}
public get redoStackSize(): number {
return this._redoStack.length;
}
}
示例使用代码
TypeScript
// src/main.ts
import { CommandManager } from "./commands/CommandManager";
import { Command } from "./commands/Command";
import { CompositeCommand } from "./commands/CompositeCommand";
// 示例命令 - 修改文本
class ChangeTextCommand extends Command {
constructor(
private _target: { text: string },
private _newText: string,
private _oldText: string
) {
super();
}
protected doExecute(): void {
this._target.text = this._newText;
console.log(`Text changed to: ${this._newText}`);
}
protected doUndo(): void {
this._target.text = this._oldText;
console.log(`Text reverted to: ${this._oldText}`);
}
}
// 示例命令 - 修改数字
class ChangeNumberCommand extends Command {
constructor(
private _target: { value: number },
private _newValue: number,
private _oldValue: number
) {
super();
}
protected doExecute(): void {
this._target.value = this._newValue;
console.log(`Number changed to: ${this._newValue}`);
}
protected doUndo(): void {
this._target.value = this._oldValue;
console.log(`Number reverted to: ${this._oldValue}`);
}
}
// 使用示例
const commandManager = new CommandManager();
const textObject = { text: "Initial text" };
const numberObject = { value: 0 };
// 创建并执行单个命令
const changeTextCommand = new ChangeTextCommand(textObject, "New text", textObject.text);
commandManager.execute(changeTextCommand);
// 创建并执行组合命令
const compositeCommand = new CompositeCommand();
compositeCommand.addCommand(new ChangeTextCommand(textObject, "Composite text", textObject.text));
compositeCommand.addCommand(new ChangeNumberCommand(numberObject, 42, numberObject.value));
commandManager.execute(compositeCommand);
// 测试 undo/redo
console.log("--- Undo ---");
commandManager.undo(); // 撤销组合命令
console.log("Current text:", textObject.text);
console.log("Current number:", numberObject.value);
console.log("--- Redo ---");
commandManager.redo(); // 重做组合命令
console.log("Current text:", textObject.text);
console.log("Current number:", numberObject.value);
console.log("--- Undo single command ---");
commandManager.undo(); // 撤销组合命令
commandManager.undo(); // 撤销第一个文本修改命令
console.log("Current text:", textObject.text);
在以上示例代码中,ChangeTextCommand类和ChangeNumberCommand类继承了 Command
类,这两个类需要:
-
持有操作的目标对象和必要参数(通常通过构造函数注入)
-
实现 具体的执行/撤销逻辑(
doExecute()
/doUndo()
)
而基类 Command
则负责:
-
管理命令的执行状态
-
提供执行流程的骨架
严格模式配置
确保你的 tsconfig.json
包含严格模式设置:
TypeScript
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
设计说明
ICommand 接口:定义了命令模式的基本操作。
这里需要说明一下,为什么先要定义一个ICommand接口,而不是直接从Command抽象类开始?主要是基于一下三点考虑:
-
允许未来添加不基于
Command
基类的其他命令实现 -
使
CommandManager
只依赖接口而不依赖具体实现(符合DIP原则) -
为组合模式(如
CompositeCommand
)提供了更清晰的类型约束
Command 抽象类:
基类 Command
额外提供了:执行状态跟踪 (_isExecuted
)防重复执行(通过检查状态),这使得子类可以更专注于业务逻辑,而不用重复编写状态管理代码。
-
实现了基础执行状态跟踪
-
要求子类实现实际执行和撤销逻辑
CompositeCommand:
-
可以组合多个命令一起执行
-
撤销时按相反顺序执行
CommandManager:
-
管理 undo/redo 堆栈
-
限制最大堆栈大小防止内存问题
-
提供清晰的API和状态查询
这个实现是完全类型安全的,符合 TypeScript 严格模式要求,并且可以直接集成到 Vite 项目中。