在Web图形编辑器开发中,“移动图形”是最基础且高频的交互操作,不仅需要保证用户拖拽时的流畅体验(图形实时跟随鼠标),还需支持撤销/重做、状态管理、灵活扩展等核心需求。很多开发者第一时间会想到命令模式,但实际上,结合场景需求,还有多种设计模式可实现移动操作,甚至能通过模式组合达到更优的代码可维护性和扩展性。
本文基于实际开发对话场景,梳理图形编辑器移动操作的核心需求,详解命令模式及其他可替代/补充的设计模式,所有示例均使用TypeScript实现,方便直接应用到项目中。
一、核心需求拆解
在动手设计前,先明确图形编辑器移动操作的核心诉求,避免设计偏离实际场景:
- 流畅交互:拖拽时图形实时跟随鼠标指针,无明显延迟;
- 状态可追溯:支持撤销/重做,仅记录完整的移动操作(而非拖拽过程中的每一步微操作);
- 可扩展性:支持多种移动规则(如自由移动、网格对齐、吸附),且能灵活新增;
- 解耦性:操作逻辑与UI渲染、状态管理分离,便于后续维护和迭代。
二、核心模式:命令模式(最常用,适配撤销/重做)
命令模式是图形编辑器移动操作的首选,核心是将“移动操作”封装为独立命令对象,解耦操作的发起者(UI交互)与执行者(图形对象),同时支持操作记录和撤销/重做。
结合拖拽场景的关键优化:拖拽过程中实时更新图形位置(保证体验),拖拽结束后生成单条命令(避免冗余记录),完全遵循“命令控制图形变化”的核心原则。
2.1 TypeScript实现
// 1. 图形基础类(接收者:真正执行移动操作的对象)
class Graphic {
constructor(
public id: string,
public x: number,
public y: number,
public width: number,
public height: number
) {}
// 核心移动方法:修改图形位置
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
// 辅助方法:判断鼠标是否点击在图形上(用于拖拽触发)
isPointInside(mouseX: number, mouseY: number): boolean {
return mouseX >= this.x && mouseX <= this.x + this.width &&
mouseY >= this.y && mouseY <= this.y + this.height;
}
}
// 2. 命令接口(规范所有命令的统一方法)
interface Command {
execute(): void;
undo(): void;
}
// 3. 移动命令(具体命令:封装移动操作)
class MoveGraphicCommand implements Command {
private initialX: number; // 移动前的初始X坐标(用于撤销)
private initialY: number; // 移动前的初始Y坐标(用于撤销)
constructor(
private graphic: Graphic,
private totalDx: number, // 总位移X(拖拽结束后计算)
private totalDy: number // 总位移Y(拖拽结束后计算)
) {
// 记录初始状态(仅初始化时记录一次)
this.initialX = graphic.x;
this.initialY = graphic.y;
}
// 执行命令:移动图形
execute(): void {
this.graphic.move(this.totalDx, this.totalDy);
}
// 撤销命令:恢复到移动前的状态
undo(): void {
this.graphic.x = this.initialX;
this.graphic.y = this.initialY;
}
}
// 4. 命令管理器(调用者:管理命令队列,实现撤销/重做)
class CommandManager {
private history: Command[] = []; // 命令历史队列
private currentIndex: number = -1; // 当前命令索引
// 执行命令(清空已撤销的命令,添加新命令)
executeCommand(command: Command): void {
if (this.currentIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.currentIndex + 1);
}
command.execute();
this.history.push(command);
this.currentIndex++;
}
// 撤销操作
undo(): void {
if (this.currentIndex >= 0) {
const command = this.history[this.currentIndex];
command.undo();
this.currentIndex--;
}
}
// 重做操作
redo(): void {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
const command = this.history[this.currentIndex];
command.execute();
}
}
}
// 5. 拖拽控制器(衔接UI交互与命令,处理实时拖拽)
class DragController {
private draggingGraphic: Graphic | null = null;
private startMouseX: number = 0;
private startMouseY: number = 0;
private startGraphicX: number = 0;
private startGraphicY: number = 0;
constructor(private commandManager: CommandManager) {}
// 开始拖拽(鼠标按下)
startDrag(graphic: Graphic, mouseX: number, mouseY: number): void {
this.draggingGraphic = graphic;
this.startMouseX = mouseX;
this.startMouseY = mouseY;
this.startGraphicX = graphic.x;
this.startGraphicY = graphic.y;
}
// 拖拽中(鼠标移动,实时更新图形位置)
drag(mouseX: number, mouseY: number): void {
if (!this.draggingGraphic) return;
// 计算实时位移
const dx = mouseX - this.startMouseX;
const dy = mouseY - this.startMouseY;
// 实时更新图形位置(仅视觉反馈,不记录命令)
this.draggingGraphic.x = this.startGraphicX + dx;
this.draggingGraphic.y = this.startGraphicY + dy;
}
// 结束拖拽(鼠标释放,生成并执行命令)
endDrag(): void {
if (!this.draggingGraphic) return;
// 计算总位移(拖拽全程的总偏移量)
const totalDx = this.draggingGraphic.x - this.startGraphicX;
const totalDy = this.draggingGraphic.y - this.startGraphicY;
// 只有位移不为0时,才生成命令(避免无效操作)
if (totalDx !== 0 || totalDy !== 0) {
const command = new MoveGraphicCommand(
this.draggingGraphic,
totalDx,
totalDy
);
this.commandManager.executeCommand(command);
}
// 重置拖拽状态
this.draggingGraphic = null;
}
}
// 6. 编辑器入口(整合所有模块,模拟UI交互)
class GraphicEditor {
private graphics: Graphic[] = [];
private commandManager = new CommandManager();
private dragController = new DragController(this.commandManager);
// 添加图形
addGraphic(graphic: Graphic): void {
this.graphics.push(graphic);
}
// 模拟鼠标按下事件(触发拖拽开始)
onMouseDown(mouseX: number, mouseY: number): void {
// 查找被点击的图形(从后往前,优先选中上层图形)
const targetGraphic = this.graphics.slice().reverse().find(graphic =>
graphic.isPointInside(mouseX, mouseY)
);
if (targetGraphic) {
this.dragController.startDrag(targetGraphic, mouseX, mouseY);
}
}
// 模拟鼠标移动事件(触发拖拽中)
onMouseMove(mouseX: number, mouseY: number): void {
this.dragController.drag(mouseX, mouseY);
this.refreshCanvas(); // 刷新画布,渲染最新位置
}
// 模拟鼠标释放事件(触发拖拽结束)
onMouseUp(): void {
this.dragController.endDrag();
this.refreshCanvas();
}
// 模拟画布刷新(实际项目中替换为DOM/Canvas渲染逻辑)
private refreshCanvas(): void {
console.log("画布刷新,当前图形状态:", this.graphics);
}
}
// 测试示例
const editor = new GraphicEditor();
// 添加一个矩形图形
const rect = new Graphic("rect1", 100, 100, 200, 100);
editor.addGraphic(rect);
// 模拟拖拽流程
editor.onMouseDown(150, 150); // 点击图形中心,开始拖拽
editor.onMouseMove(250, 200); // 拖拽到新位置
editor.onMouseUp(); // 释放鼠标,生成移动命令
console.log("拖拽结束后图形位置:", rect.x, rect.y); // 200, 150
editor.commandManager.undo(); // 撤销移动
console.log("撤销后图形位置:", rect.x, rect.y); // 100, 100
editor.commandManager.redo(); // 重做移动
console.log("重做后图形位置:", rect.x, rect.y); // 200, 150
2.2 关键说明
- 分离“视觉反馈”与“命令执行”:拖拽过程中直接修改图形位置(保证流畅),拖拽结束后才生成单条命令(避免冗余);
- 命令封装完整状态:移动命令记录图形初始位置,确保撤销时能精准恢复;
- 命令管理器统一管理:负责命令的执行、撤销、重做,解耦UI交互与命令逻辑。
三、其他可用设计模式(结合场景补充)
命令模式虽常用,但在特定场景下,其他设计模式可更好地解决问题(如状态管理、移动规则扩展、多图形联动、状态备份等)。以下结合图形编辑器移动操作,介绍6种实用设计模式(含对话中提及的备忘录模式),均提供TS示例,覆盖所有相关场景,且各模式间形成互补,方便开发者根据需求灵活选用。
3.1 策略模式:适配多种移动规则
核心思想:定义多种移动算法(如自由移动、网格对齐、水平/垂直锁定),封装为独立策略,可动态切换,无需修改图形或命令代码。
适用场景:需要支持多种移动规则,且规则可灵活扩展(如新增“吸附到参考线”功能)。
// 1. 移动策略接口(规范所有移动算法)
interface MoveStrategy {
calculateNewPosition(
currentX: number,
currentY: number,
dx: number,
dy: number
): { x: number; y: number };
}
// 2. 具体策略1:自由移动(默认)
class FreeMoveStrategy implements MoveStrategy {
calculateNewPosition(currentX: number, currentY: number, dx: number, dy: number): { x: number; y: number } {
return { x: currentX + dx, y: currentY + dy };
}
}
// 3. 具体策略2:网格对齐(按指定网格大小移动)
class GridMoveStrategy implements MoveStrategy {
constructor(private gridSize: number = 20) {}
calculateNewPosition(currentX: number, currentY: number, dx: number, dy: number): { x: number; y: number } {
// 计算对齐网格后的位置
const newX = Math.round((currentX + dx) / this.gridSize) * this.gridSize;
const newY = Math.round((currentY + dy) / this.gridSize) * this.gridSize;
return { x: newX, y: newY };
}
}
// 4. 改造图形类,支持设置移动策略
class GraphicWithStrategy {
public moveStrategy: MoveStrategy = new FreeMoveStrategy(); // 默认自由移动
constructor(
public id: string,
public x: number,
public y: number,
public width: number,
public height: number
) {}
// 结合策略移动图形
move(dx: number, dy: number): void {
const { x, y } = this.moveStrategy.calculateNewPosition(this.x, this.y, dx, dy);
this.x = x;
this.y = y;
}
}
// 测试示例
const rect = new GraphicWithStrategy("rect1", 100, 100, 200, 100);
// 切换为网格对齐策略(网格大小20)
rect.moveStrategy = new GridMoveStrategy(20);
rect.move(35, 45); // 原本移动35,45,对齐后为40,60(20的倍数)
console.log(rect.x, rect.y); // 140, 160
3.2 状态模式:管理编辑器交互状态
核心思想:将编辑器的不同交互状态(选择模式、移动模式、缩放模式)封装为独立状态类,状态切换时自动改变行为,避免大量if-else判断。
适用场景:编辑器有多种交互模式,移动操作仅在“移动模式”下生效。
// 1. 状态接口(规范所有状态的行为)
interface EditorState {
handleMouseDown(editor: StatefulEditor, mouseX: number, mouseY: number): void;
handleMouseMove(editor: StatefulEditor, mouseX: number, mouseY: number): void;
handleMouseUp(editor: StatefulEditor): void;
}
// 2. 选择状态(默认状态:点击选中图形,不移动)
class SelectState implements EditorState {
handleMouseDown(editor: StatefulEditor, mouseX: number, mouseY: number): void {
const targetGraphic = editor.graphics.find(g => g.isPointInside(mouseX, mouseY));
if (targetGraphic) {
editor.selectedGraphic = targetGraphic;
// 切换到移动状态(点击选中后,拖拽即移动)
editor.setState(new MoveState());
}
}
handleMouseMove(editor: StatefulEditor, mouseX: number, mouseY: number): void {
// 选择状态下,鼠标移动不做任何操作
}
handleMouseUp(editor: StatefulEditor): void {
// 选择状态下,鼠标释放不做任何操作
}
}
// 3. 移动状态(拖拽移动选中的图形)
class MoveState implements EditorState {
private startMouseX: number = 0;
private startMouseY: number = 0;
private startGraphicX: number = 0;
private startGraphicY: number = 0;
handleMouseDown(editor: StatefulEditor, mouseX: number, mouseY: number): void {
if (editor.selectedGraphic) {
this.startMouseX = mouseX;
this.startMouseY = mouseY;
this.startGraphicX = editor.selectedGraphic.x;
this.startGraphicY = editor.selectedGraphic.y;
}
}
handleMouseMove(editor: StatefulEditor, mouseX: number, mouseY: number): void {
if (!editor.selectedGraphic) return;
const dx = mouseX - this.startMouseX;
const dy = mouseY - this.startMouseY;
editor.selectedGraphic.x = this.startGraphicX + dx;
editor.selectedGraphic.y = this.startGraphicY + dy;
editor.refreshCanvas();
}
handleMouseUp(editor: StatefulEditor): void {
// 移动结束,切换回选择状态
editor.setState(new SelectState());
// 生成移动命令(结合命令模式,支持撤销)
if (editor.selectedGraphic) {
const totalDx = editor.selectedGraphic.x - this.startGraphicX;
const totalDy = editor.selectedGraphic.y - this.startGraphicY;
if (totalDx !== 0 || totalDy !== 0) {
const command = new MoveGraphicCommand(
editor.selectedGraphic,
totalDx,
totalDy
);
editor.commandManager.executeCommand(command);
}
}
}
}
// 4. 带状态的编辑器
class StatefulEditor {
public graphics: Graphic[] = [];
public selectedGraphic: Graphic | null = null;
public state: EditorState = new SelectState(); // 默认选择状态
public commandManager = new CommandManager();
// 设置编辑器状态
setState(state: EditorState): void {
this.state = state;
}
// 转发鼠标事件到当前状态
onMouseDown(mouseX: number, mouseY: number): void {
this.state.handleMouseDown(this, mouseX, mouseY);
}
onMouseMove(mouseX: number, mouseY: number): void {
this.state.handleMouseMove(this, mouseX, mouseY);
}
onMouseUp(): void {
this.state.handleMouseUp(this);
}
refreshCanvas(): void {
console.log("画布刷新,当前图形状态:", this.graphics);
}
}
3.3 组合模式:支持多图形群组移动
核心思想:将单个图形(叶子节点)和多个图形的组合(组合节点)统一视为“图形组件”,使客户端对单个图形和组合图形的移动操作具有一致性。
适用场景:需要支持“选中多个图形,批量移动”功能。
// 1. 图形组件接口(统一叶子和组合节点的行为)
interface GraphicComponent {
id: string;
move(dx: number, dy: number): void;
isPointInside(mouseX: number, mouseY: number): boolean;
}
// 2. 叶子节点:单个图形
class LeafGraphic implements GraphicComponent {
constructor(
public id: string,
public x: number,
public y: number,
public width: number,
public height: number
) {}
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
isPointInside(mouseX: number, mouseY: number): boolean {
return mouseX >= this.x && mouseX <= this.x + this.width &&
mouseY >= this.y && mouseY <= this.y + this.height;
}
}
// 3. 组合节点:多个图形的群组
class CompositeGraphic implements GraphicComponent {
public children: GraphicComponent[] = [];
constructor(public id: string) {}
// 添加图形到群组
add(component: GraphicComponent): void {
this.children.push(component);
}
// 从群组移除图形
remove(component: GraphicComponent): void {
this.children = this.children.filter(c => c.id !== component.id);
}
// 群组移动:所有子图形同步移动
move(dx: number, dy: number): void {
this.children.forEach(child => child.move(dx, dy));
}
// 判断鼠标是否点击在群组内(任意子图形被点击即视为选中群组)
isPointInside(mouseX: number, mouseY: number): boolean {
return this.children.some(child => child.isPointInside(mouseX, mouseY));
}
}
// 测试示例
// 创建两个单个图形
const rect1 = new LeafGraphic("rect1", 100, 100, 100, 50);
const rect2 = new LeafGraphic("rect2", 200, 200, 100, 50);
// 创建群组,添加两个图形
const group = new CompositeGraphic("group1");
group.add(rect1);
group.add(rect2);
// 移动群组(两个图形同步移动)
group.move(50, 50);
console.log(rect1.x, rect1.y); // 150, 150
console.log(rect2.x, rect2.y); // 250, 250
3.4 观察者模式:实现状态联动更新
核心思想:定义对象间的一对多依赖,当图形位置(被观察者)变化时,所有依赖它的组件(观察者,如画布、属性面板)自动收到通知并更新。
适用场景:图形移动后,需要同步更新画布渲染、属性面板的坐标显示等。
// 1. 观察者接口
interface Observer {
update(subject: Subject): void;
}
// 2. 被观察者基类(图形继承此类)
class Subject {
private observers: Observer[] = [];
// 注册观察者
attach(observer: Observer): void {
this.observers.push(observer);
}
// 移除观察者
detach(observer: Observer): void {
this.observers = this.observers.filter(o => o !== observer);
}
// 通知所有观察者
protected notify(): void {
this.observers.forEach(observer => observer.update(this));
}
}
// 3. 可观察的图形类(被观察者)
class ObservableGraphic extends Subject {
constructor(
public id: string,
private _x: number,
private _y: number,
public width: number,
public height: number
) {
super();
}
// 访问器:修改x/y时通知观察者
get x(): number {
return this._x;
}
set x(value: number) {
this._x = value;
this.notify(); // 位置变化,通知观察者
}
get y(): number {
return this._y;
}
set y(value: number) {
this._y = value;
this.notify(); // 位置变化,通知观察者
}
// 移动方法
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
}
// 4. 观察者1:画布(更新渲染)
class CanvasObserver implements Observer {
update(subject: Subject): void {
if (subject instanceof ObservableGraphic) {
console.log(`画布更新:图形${subject.id}移动到(${subject.x}, ${subject.y})`);
}
}
}
// 5. 观察者2:属性面板(更新坐标显示)
class PropertyPanelObserver implements Observer {
update(subject: Subject): void {
if (subject instanceof ObservableGraphic) {
console.log(`属性面板更新:图形${subject.id}坐标 - X: ${subject.x}, Y: ${subject.y}`);
}
}
}
// 测试示例
const graphic = new ObservableGraphic("rect1", 100, 100, 200, 100);
const canvas = new CanvasObserver();
const propertyPanel = new PropertyPanelObserver();
// 注册观察者
graphic.attach(canvas);
graphic.attach(propertyPanel);
// 移动图形,触发观察者更新
graphic.move(50, 50);
// 输出:
// 画布更新:图形rect1移动到(150, 150)
// 属性面板更新:图形rect1坐标 - X: 150, Y: 150
3.5 原型模式:拖拽预览与状态备份
核心思想:通过复制现有图形(原型)创建新对象,无需重新初始化,高效实现拖拽预览、撤销时的状态备份。
适用场景:拖拽时需要显示“预览图形”(不影响原图形),或撤销时需要快速恢复图形状态(适合简单图形,无需额外筛选核心状态),与备忘录模式形成互补。
// 1. 原型接口(定义克隆方法)
interface Prototype {
clone(): Prototype;
}
// 2. 可克隆的图形类
class CloneableGraphic implements Prototype {
constructor(
public id: string,
public x: number,
public y: number,
public width: number,
public height: number,
public color: string = "#000000"
) {}
// 克隆方法:创建当前图形的副本
clone(): CloneableGraphic {
return new CloneableGraphic(
`${this.id}_clone`, // 克隆体ID区分原图形
this.x,
this.y,
this.width,
this.height,
this.color
);
}
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
}
// 测试示例(拖拽预览)
const originalGraphic = new CloneableGraphic("rect1", 100, 100, 200, 100, "#ff0000");
// 克隆图形作为预览(拖拽时移动预览,不影响原图形)
const previewGraphic = originalGraphic.clone();
// 拖拽预览图形
previewGraphic.move(50, 50);
console.log("原图形位置:", originalGraphic.x, originalGraphic.y); // 100, 100
console.log("预览图形位置:", previewGraphic.x, previewGraphic.y); // 150, 150
// 拖拽结束,将原图形移动到预览位置
originalGraphic.move(50, 50);
console.log("拖拽结束后原图形位置:", originalGraphic.x, originalGraphic.y); // 150, 150
3.6 备忘录模式:图形状态备份与恢复
核心思想:在不破坏对象封装性的前提下,捕获对象的内部状态并保存,以便后续需要时恢复到该状态。与原型模式的“复制对象”不同,备忘录模式仅保存对象的关键状态,更轻量、更聚焦“状态回溯”。
适用场景:图形移动、修改属性等操作后,需要精准恢复到操作前的状态(如撤销移动时,无需复制整个图形,仅恢复位置状态),常与命令模式协作实现完整的撤销/重做功能,尤其适合复杂图形(属性较多)的场景,可弥补原型模式“复制完整对象”的性能损耗。
// 1. 备忘录类(存储图形的关键状态,不可直接修改)
class GraphicMemento {
// 仅存储移动相关的关键状态(x、y坐标),按需扩展
constructor(public readonly x: number, public readonly y: number) {}
}
// 2. 原发器(图形类):创建和恢复备忘录
class MementoGraphic {
constructor(
public id: string,
public x: number,
public y: number,
public width: number,
public height: number
) {}
// 移动图形
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
// 创建备忘录:保存当前状态
createMemento(): GraphicMemento {
return new GraphicMemento(this.x, this.y);
}
// 恢复备忘录:从备忘录中恢复状态
restoreMemento(memento: GraphicMemento): void {
this.x = memento.x;
this.y = memento.y;
}
}
// 3. 管理者(可选):负责存储备忘录,避免原发器直接操作备忘录
class MementoManager {
private mementos: Map<string, GraphicMemento> = new Map(); // key: 图形ID,value: 备忘录
// 保存备忘录
saveMemento(graphicId: string, memento: GraphicMemento): void {
this.mementos.set(graphicId, memento);
}
// 获取备忘录
getMemento(graphicId: string): GraphicMemento | undefined {
return this.mementos.get(graphicId);
}
}
// 4. 结合命令模式使用(完善撤销逻辑)
class MementoMoveCommand implements Command {
private memento: GraphicMemento; // 保存移动前的状态(备忘录)
constructor(
private graphic: MementoGraphic,
private dx: number,
private dy: number,
private mementoManager: MementoManager
) {
// 执行命令前,创建并保存备忘录(移动前的状态)
this.memento = this.graphic.createMemento();
this.mementoManager.saveMemento(this.graphic.id, this.memento);
}
execute(): void {
this.graphic.move(this.dx, this.dy);
}
undo(): void {
// 从备忘录恢复到移动前的状态
const memento = this.mementoManager.getMemento(this.graphic.id);
if (memento) {
this.graphic.restoreMemento(memento);
}
}
}
// 测试示例
const mementoManager = new MementoManager();
const graphic = new MementoGraphic("rect1", 100, 100, 200, 100);
// 创建移动命令,自动保存备忘录
const moveCommand = new MementoMoveCommand(graphic, 50, 50, mementoManager);
moveCommand.execute();
console.log("移动后图形位置:", graphic.x, graphic.y); // 150, 150
// 撤销移动,从备忘录恢复状态
moveCommand.undo();
console.log("撤销后图形位置:", graphic.x, graphic.y); // 100, 100
关键说明:备忘录模式专注于“状态备份与恢复”,与命令模式协作时,命令负责执行操作,备忘录负责保存操作前后的关键状态,让撤销逻辑更简洁、更精准。尤其适合复杂图形(属性较多)的状态回溯,相比原型模式的“复制整个对象”,备忘录仅保存核心状态,更轻量、更节省内存——这也是它与原型模式在状态备份场景中的核心区别,二者相辅相成,可根据图形复杂度灵活选择,与前文原型模式的适用场景形成精准呼应。
四、模式选择与组合建议
单一设计模式难以满足图形编辑器的复杂需求,实际开发中建议根据场景组合使用,以下是高频组合方案:
4.1 常用组合方案
- 命令模式 + 策略模式:用命令模式管理移动操作(支持撤销),用策略模式切换移动规则(自由/网格/吸附);
- 命令模式 + 组合模式:用组合模式管理群组图形,用命令模式实现群组移动的撤销/重做;
- 状态模式 + 观察者模式:用状态模式管理编辑器交互状态,用观察者模式实现图形移动后的联动更新;
- 命令模式 + 原型模式:用原型模式备份图形初始状态,用命令模式实现撤销时的状态恢复(适合简单图形、拖拽预览场景);
- 命令模式 + 备忘录模式:用备忘录模式轻量保存图形操作前的关键状态,用命令模式管理操作执行与撤销,兼顾性能与精准性,适配复杂图形的状态回溯需求。
4.2 模式选择对照表
| 设计模式 | 核心优势 | 适用场景 |
|---|---|---|
| 命令模式 | 支持撤销/重做,解耦操作发起与执行 | 需要记录操作历史,支持撤销/重做 |
| 策略模式 | 移动规则可扩展、可切换,无需修改核心代码 | 支持多种移动规则(自由、网格、吸附) |
| 状态模式 | 简化交互状态管理,避免大量if-else | 编辑器有多种交互模式(选择、移动、缩放) |
| 组合模式 | 统一单个图形与群组的操作逻辑 | 需要支持多图形群组移动 |
| 观察者模式 | 状态变化自动联动更新,解耦组件依赖 | 图形移动后需同步更新画布、属性面板等 |
| 原型模式 | 高效复制对象,用于预览、状态备份 | 拖拽预览、撤销时的状态恢复(适合简单图形) |
| 备忘录模式 | 轻量保存对象关键状态,不破坏封装,精准恢复 | 复杂图形状态备份、与命令模式协作实现撤销 |
五、总结
图形编辑器的移动操作设计,核心是平衡“用户体验”与“代码可维护性”:命令模式是基础,解决撤销/重做和操作解耦;策略模式、状态模式、备忘录模式等用于补充扩展,分别解决移动规则、交互状态、状态备份等细分问题;模式组合则能应对更复杂的场景(如群组移动、多组件联动、复杂图形状态回溯)。
本文所有示例均基于TypeScript实现,可直接复制到项目中修改适配,重点关注“命令模式+策略模式”“命令模式+组合模式”“命令模式+备忘录模式”这三组高频组合,基本能覆盖大部分图形编辑器移动操作的需求。尤其值得注意的是,备忘录模式与原型模式虽都可用于状态备份,但场景各有侧重——备忘录模式轻量保存关键状态,适配复杂图形;原型模式复制完整对象,适配拖拽预览等场景,合理区分二者可进一步优化项目性能。
如果你的编辑器有更特殊的场景(如异步移动、复杂吸附规则),可基于上述模式进一步扩展,核心原则是:将变化的部分封装起来,降低组件间的耦合,让代码更易维护、易扩展。