【问题标题】:Command pattern how to solve issue with dependencies?命令模式如何解决依赖问题?
【发布时间】:2021-04-17 17:05:35
【问题描述】:

我有一个具体的按钮类:

class ContourButton implements Button {
    public icon: string;
    public title: string;

    constructor(public command: Command) {
    }
}

此按钮接受单击后应执行的命令。

命令如下:

export class ContourCommand implements Command {
    constructor(public action: EditAction) {}

    undo(): void {
        this.action.undo();
    }
    redo(): void {
        // TODO
    }
    execute(): void {
        this.action.execute();
    }

    complete(): void {
        this.action.complete();
    }
}

这个类提供了一个 reciever 作为 public action: EditAction 包含业务逻辑。

还有一个命令管理器,它将命令添加到堆栈:

class CommandManager {
    public currentCommand: Command;
    protected commands: Command[] = [];
    protected undoCommand: Command;

    execute(command: Command): void {
        if (command === this.currentCommand) return;
        this.currentCommand = command;
        this.commands.push(this.currentCommand);
        this.currentCommand.execute();
    }
}

我不喜欢这行代码:

 const command = new ContourCommand(new EditorManager(new Type()));
 const button1 = new ContourButton(command);
 const button2 = new PinButton(command);

 const buttons = [button1, button2];

模板为:

<div *ngFor="let button of buttons" (click)="btnClick($event)"></div>

手柄点击:

public btnClick(button: Button) {
    this.commandManager.execute(button.command);
}

因为要创建一个命令,我需要将一个接收者实体指定为new EditorManager(),它具有自己的依赖项。

我是否正确使用了这种模式,以及如何解决这种依赖关系的副作用?我把它写成库,所以我这里没有任何 DI 机制。

【问题讨论】:

  • 我完全不明白 ContourButton 为什么要创建一个新命令。它已经需要一个命令作为参数!好像你不喜欢的可以完全删除。
  • 抱歉打错字了,我改正了
  • 另外你的 CommandManager 类可以被清理。 this.currentCommand 不就是 this.commands 数组中的最后一项吗?如果命令是可撤销的和加倍的​​(这样当前并不总是最后一个),那么您想知道的信息就是数组中的当前索引。 currentCommand 可以从类似 this.commands[this.currentIndex] 中派生出来。
  • 是的,但我需要在课堂上的某处封装命令:[]。这就是 commandManagerExist 的原因。或者你的意思是我不需要变量currentCommand: Command;
  • ContourCommand 什么都不做?或者您是否删除了逻辑以使其更像是一个最小的示例?它只是包装了一个已经可以执行的 EditAction。所以你可以直接将 EditorManager 传递给 Button。

标签: javascript typescript oop


【解决方案1】:

命令管理器

你和我在chat 中一起工作了很长时间。我们意识到CommandManager 的“工作”是管理用于undo()redo() 操作的操作堆栈。

CommandManager 应该存储当前操作的index。我们在添加或重做动作时递增索引,在撤消时递减索引。在撤消某些操作后添加新操作时,我们需要删除所有可重做的“未来”命令。新命令需要位于正确的索引处,并且还需要位于堆栈的顶部。

您的各个对象不是通过CommandManager 执行操作,而是根据用户事件自行执行某些操作。然后他们将这个动作“注册”到CommandManager 并提供undo()redo() 方法。这些方法是基于原始操作参数的闭包。简单来说,如果一个对象的value 为 5 并且调用了setValue(10),那么它会将这个操作作为{undo: () =&gt; setValue(5), redo: () =&gt; setValue(10)} 发送到CommandManager


如果我们将Command 定义为带有返回undo()redo() 函数的execute() 方法,我们可以使其与更传统的命令模式设计兼容。

interface RegisteredAction {
  undo(): void;
  redo(): void;
}

interface Command {
    execute(): RegisteredAction;
}

export class CommandManager {
  protected commands: RegisteredAction[] = [];
  protected currentIndex = -1;

  // add a command to the stack which was executed elsewhere
  didExecute(command: RegisteredAction): void {
    this.currentIndex++;
    this.commands.splice(this.currentIndex, this.commands.length, command);
  }

  // trigger execution of a command from here
  execute(command: Command): void {
      const action = command.execute();
      this.didExecute(action);
  }

  // the command to undo
  get currentCommand(): RegisteredAction | undefined {
    return this.commands[this.currentIndex];
  }

   // the command to redo
  get nextCommand(): RegisteredAction | undefined {
    return this.commands[this.currentIndex + 1];
  }

  // whether on not the current stack can be undone
  get canUndo(): boolean {
    return !!this.currentCommand;
  }

  // whether on not the current stack supports redo
  get canRedo(): boolean {
    return !!this.nextCommand;
  }

  // undo one command, if possible
  // could potentially return a boolean that indicates if an undo occurred
  undo(): void {
    if (this.currentCommand) {
      this.currentCommand.undo();
      this.currentIndex--;
    }
  }

  // redo one command, if possible
  redo(): void {
    if (this.nextCommand) {
      this.nextCommand.redo();
      this.currentIndex++;
    }
  }

  // undo all commands
  cancel(): void {
    while (this.currentIndex >= 0) {
      this.undo();
    }
  }
}

Typescript Playground Link


按钮

您最初认为应用程序中的每个按钮都将绑定到Command。实际上,我们的“撤消”和“重做”按钮将绑定到CommandManager。我们在CommandManager 中包含了canUndocanRedo 属性,以便我们可以知道这些按钮的disabled 状态。

export interface Button {
  icon: string;
  title: string;
  disabled?: boolean;
  onClick(): void;
}

class UndoButton implements Button {
  icon = "undoicon"; //placeholder
  title = "Undo";
  constructor(private manager: CommandManager) {}

  onClick() {
    this.manager.undo();
  }

  get disabled() {
    return !this.manager.canUndo;
  }
}

class RedoButton implements Button {
  icon = "redoicon"; //placeholder
  title = "Redo";
  constructor(private manager: CommandManager) {}

  onClick() {
    this.manager.redo();
  }

  get disabled() {
    return !this.manager.canRedo;
  }
}

【讨论】: