前言

命令模式(Command Pattern)是一种行为设计模式,它将一个请求或简单操作封装成一个对象,从而使你可以使用不同的请求、队列或日志请求来参数化其他对象。同时支持可撤销的操作。命令模式主要包含以下几个角色:

  1. 命令(Command)接口 - 声明执行操作的接口。此接口通常定义了一个执行具体命令的方法。
  2. 具体命令(Concrete Command) - Command 接口的实现对象,它定义了绑定在接收者上的动作之间的关联。当命令的 execute() 方法被调用时,将会把动作转发给接收者去执行。
  3. 客户(Client) - 创建一个具体命令对象并设置其接收者。有时客户负责创建并设置命令对象的接收者,这一角色可以看作是请求的发起者。
  4. 请求者(Invoker) - 负责调用命令对象执行请求,通常会持有命令对象(可以是一个或者一组命令)并在某个时间点调用命令对象的 execute() 方法。
  5. 接收者(Receiver) - 知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者,通常会有实际操作的具体逻辑。

命令模式的优点在于它可以将发起操作的对象和执行操作的对象解耦,使得发起者不需要了解具体的接收者信息。此外,它支持撤销操作,可以用来实现请求的记录、队列、撤销和重做功能。

上面的话可能很笼统,实际上命令模式核心就是抽象和封装,将具体的操作逻辑封装成接收者,再将它封装到统一的命令对象中去,由于命令对象是固定接口的,所以它可以很放心的给请求者去执行,而请求者可以理解为具体执行代码命令对象的代码,而客户就是去创建并组合它们的。

基于这种行为,使得客户具体的执行逻辑实现了解耦,客户不在关心你的具体是怎么执行的,它的函数里就不需要写具体的操作了,它只需要通过请求者去执行封装好的命令对象即可,而命令对象和接收者虽然存在耦合关系,但是也可以通过再向上转型,依赖一个接口或者抽象,也能实现弱耦合。

这种解耦,使得我们对命令的扩展(具体的操作逻辑)非常方便,比如实现队列动画,撤销操作等。

仿强类型语言的命令模式实现

// 命令接口
class Command {
    execute() {}
    undo() {}
}

// 调用者
class RemoteControl {
    constructor() {
        this.command = null;
    }

    /** 组合的方式来持有接收者 */
    setCommand(command) {
        this.command = command;
    }

    /** 执行命令 */
    pressButton() {
        if (this.command) {
            this.command.execute();
        } else {
            console.log("No command is set!");
        }
    }

    /** 撤销命令 */
    pressUndo() {
        if (this.command) {
            this.command.undo();
        } else {
            console.log("No command to undo!");
        }
    }
}

// 接收者
class Light {
    on() {
        console.log("Light is on");
    }

    off() {
        console.log("Light is off");
    }
}

// 开灯命令
class LightOnCommand extends Command {
    constructor(light) {
        super();
        this.light = light;
    }

    execute() {
        this.light.on();
    }

    undo() {
        this.light.off();
    }
}

// 关灯命令
class LightOffCommand extends Command {
    constructor(light) {
        super();
        this.light = light;
    }

    execute() {
        this.light.off();
    }

    undo() {
        this.light.on();
    }
}

// 接收者实例
const livingRoomLight = new Light();

// 命令实例
const lightOn = new LightOnCommand(livingRoomLight);
const lightOff = new LightOffCommand(livingRoomLight);

// 调用者实例
const remote = new RemoteControl();

// 设置命令并使用
remote.setCommand(lightOn);
remote.pressButton(); // 输出:Light is on
remote.pressUndo(); // 输出:Light is off

remote.setCommand(lightOff);
remote.pressButton(); // 输出:Light is off
remote.pressUndo(); // 输出:Light is on

调用者由于命令的实力对象是固定接口的,所以完全可以复用调用者实例。

命令对象的具体实现:LightOnCommandLightOffCommand,由于接口Command没有约束构造函数,所以在子类实现的时候,它是很自由的。

下面是ts的实现:

// 命令接口
interface Command {
    execute(): void;
    undo(): void;
}

// 调用者
class RemoteControl {
    private command: Command | null;

    constructor() {
        this.command = null;
    }

    /** 组合的方式来持有接收者 */
    setCommand(command: Command) {
        this.command = command;
    }

    /** 执行命令 */
    pressButton() {
        if (this.command) {
            this.command.execute();
        } else {
            console.log("No command is set!");
        }
    }

    /** 撤销命令 */
    pressUndo() {
        if (this.command) {
            this.command.undo();
        } else {
            console.log("No command to undo!");
        }
    }
}

// 接收者
class Light {
    on() {
        console.log("Light is on");
    }

    off() {
        console.log("Light is off");
    }
}

// 开灯命令
class LightOnCommand implements Command {
    private light: Light;

    constructor(light: Light) {
        this.light = light;
    }

    execute() {
        this.light.on();
    }

    undo() {
        this.light.off();
    }
}

// 关灯命令
class LightOffCommand implements Command {
    private light: Light;

    constructor(light: Light) {
        this.light = light;
    }

    execute() {
        this.light.off();
    }

    undo() {
        this.light.on();
    }
}

// 接收者实例
const livingRoomLight = new Light();

// 命令实例
const lightOn = new LightOnCommand(livingRoomLight);
const lightOff = new LightOffCommand(livingRoomLight);

// 调用者实例
const remote = new RemoteControl();

// 设置命令并使用
remote.setCommand(lightOn);
remote.pressButton(); // 输出:Light is on
remote.pressUndo(); // 输出:Light is off

remote.setCommand(lightOff);
remote.pressButton(); // 输出:Light is off
remote.pressUndo(); // 输出:Light is on

js特性实现的命令模式

在js中,函数是一等公民,class类创建实例对象本身也是弱化的,所以我们完全可以使用js的特性去实现:

// 命令创建函数
function createCommand(execute, undo) {
    return {
        execute,
        undo
    };
}

// 调用者
const remoteControl = {
    command: null,
    setCommand(command) {
        this.command = command;
    },
    pressButton() {
        this.command ? this.command.execute() : console.log("No command is set!");
    },
    pressUndo() {
        this.command ? this.command.undo() : console.log("No command to undo!");
    },
};

// 接收者
const light = {
    on() {
        console.log("Light is on");
    },
    off() {
        console.log("Light is off");
    },
};

// 命令实例
const lightOn = createCommand(
    () => light.on(),
    () => light.off()
);
const lightOff = createCommand(
    () => light.off(),
    () => light.on()
);

// 使用
remoteControl.setCommand(lightOn);
remoteControl.pressButton(); // 输出:Light is on
remoteControl.pressUndo(); // 输出:Light is off

remoteControl.setCommand(lightOff);
remoteControl.pressButton(); // 输出:Light is off
remoteControl.pressUndo(); // 输出:Light is on

这种方式更加简洁,针对不同的业务逻辑,这种方式还会有不同的变动。

撤销命令

命令模式实现撤销的效果,就是在执行具体的逻辑之前,保存一份执行前的状态,然后当运行undo撤销还原的时候,将之前的状态数据还原。

// 命令接口
class Command {
    execute() {}
    undo() {}
}

// 调用者
class RemoteControl {
    constructor() {
        this.command = null;
    }

    /** 组合的方式来持有接收者 */
    setCommand(command) {
        this.command = command;
    }

    /** 执行命令 */
    pressButton() {
        if (this.command) {
            this.command.execute();
        } else {
            console.log("No command is set!");
        }
    }

    /** 撤销命令 */
    pressUndo() {
        if (this.command) {
            this.command.undo();
        } else {
            console.log("No command to undo!");
        }
    }
}

// 接收者
class ChangeDivWidth {
    constructor(element) {
        this.element = element;
    }

    setWidth(width) {
        this.element.style.width = width;
    }
}

// 变大命名
class BiggerCommand extends Command {
    constructor(changeDivWidth) {
        super();
        this.changeDivWidth = changeDivWidth;
        this.oldWidth = null;
    }

    execute() {
        // 获取当前div的计算后的宽度
        this.oldWidth = window.getComputedStyle(
            this.changeDivWidth.element
        ).width;
        this.changeDivWidth.setWidth("200px");
    }

    undo() {
        this.changeDivWidth.setWidth(this.oldWidth);
    }
}

// 变小命令
class SmallerCommand extends Command {
    constructor(changeDivWidth) {
        super();
        this.changeDivWidth = changeDivWidth;
        this.oldWidth = null;
    }

    execute() {
        // 获取当前div的计算后的宽度
        this.oldWidth = window.getComputedStyle(
            this.changeDivWidth.element
        ).width;
        this.changeDivWidth.setWidth("100px");
    }

    undo() {
        this.changeDivWidth.setWidth(this.oldWidth);
    }
}

// 接收者实例
const changeDivWidth = new ChangeDivWidth(
    document.querySelector(".content")
);

// 命令实例
const biggerCommand = new BiggerCommand(changeDivWidth);
const smallerCommand = new SmallerCommand(changeDivWidth);

// 调用者实例
const remoteControl = new RemoteControl();

// 设置命令
remoteControl.setCommand(biggerCommand);
remoteControl.pressButton(); // 变大
remoteControl.pressUndo(); // 撤销

// 设置命令
remoteControl.setCommand(smallerCommand);
remoteControl.pressButton(); // 变小
remoteControl.pressUndo(); // 撤销

这种方式虽然可以实现撤销,但是你必须显示声明持有已运行的命令,在正式的场合中,显然每次运行一个命令就要持有他的实例对象,是很不现实的,也不方便,所以常见的做法就是通过一个队列来处理,调用者会有一个数组,用于存放每次执行的命令,然后就可以指定还原的步数,更加便捷的还原内容。

我们稍微调整一下调用者:

// 调用者
class RemoteControl {
  constructor() {
    this.commandQueue = []; // 命令队列
    this.currentStep = -1; // 当前的步数,用于还原
  }

  /** 组合的方式来持有接收者 */
  setCommand(command) {
    // 如果不是在最后一个步骤执行新命令,则删除当前步骤后面的命令
    if (this.currentStep < this.commandQueue.length - 1) {
      this.commandQueue = this.commandQueue.slice(
        0,
        this.currentStep + 1
      );
    }

    this.commandQueue.push(command);
    this.currentStep++;
  }

  /** 执行命令 */
  pressButton() {
    if (
      this.currentStep >= 0 &&
      this.currentStep < this.commandQueue.length
    ) {
      this.commandQueue[this.currentStep].execute();
    } else {
      console.log("No command is set!");
    }
  }

  /** 撤销命令 */
  pressUndo(steps = 1) {
    if (this.currentStep - steps >= 0) {
      this.commandQueue[this.currentStep].undo();
      this.currentStep -= steps;
    } else if (this.commandQueue.length > 0) {
      this.commandQueue[0].undo();
      this.currentStep = 0;
    } else {
      console.log("No command is set!");
    }
  }
}


// 调用者实例
const remoteControl = new RemoteControl();

// 设置命令
remoteControl.setCommand(biggerCommand);
remoteControl.pressButton(); // 变大
remoteControl.setCommand(smallerCommand);
remoteControl.pressButton(); // 变小

remoteControl.pressUndo(2); // 还原

事实上除了这种记录旧数据的方式还原,还有另一种还原方式,比如以canvas绘制为例,显然我们记录旧数据的方式是很低效的,我们应该利用命令队列,从开头到结束,重新绘制一遍才是最优解,也就是重新执行一遍命令数组。

命令队列

上面用于还原撤销的操作,其实就是使用到了命令队列,使用一个数组来存放命令实例,利用这个数组我们还可以进行功能扩展。

比如动画队列,我们可以将不同的动画封装成一个个命令,然后在按顺序执行命令队列,从而实现一个动画效果。

除了动画队列,我们还可以用它来实现宏功能,宏其实也就是按照命令队列顺序依次执行,如我们打游戏常用的鼠标宏,不就是一个个固定的命令按顺序执行的结果吗?

智能命令

如果一个命令实例里不包含任何接收者实例,那么它本身就包揽了执行请求的行为,但是一般来说,命令对象都会包含一个接收者,通过操作接收者来运行具体的逻辑,这种情况下命令对象是“傻瓜式”的,它只负责把客户的请求转交给接收者去执行,这样使得请求者与接收者之间尽可能的解耦。

如果命令对象本身就包含了接收者的逻辑行为,它自身就可以执行请求,那么就被称为“聪明的”,这种聪明的命令就是智能命令。

这种方式和策略模式非常相似,甚至很难从代码结构上去区分,能分辨的只有它们的意图,策略模式的策略往往是更小范围的处理,而智能模式往往是更大范围的处理,承载的逻辑处理更多。

小结

命令模式虽然可以利用js的特性很便捷的是去实现,但是带来功能和阅读上的不便,使用class虽然繁琐,但是阅读上更加通顺,但是两者都得去了解。

分类: JavaScript设计模式与开发实践 标签: JavaScript模式命令模式

评论

暂无评论数据

暂无评论数据

目录