Head First 命令模式

0 阅读7分钟

一、定义

命令模式:将请求封装成对象,这可以让你使用不同的请求、队列、或者日志请求来参数化其他对象。命令模式也可以支持撤销操作。 它是一种对象行为型模式。
仔细看这个定义,我们知道一个命令对象通过在特定接收者上绑定一组动作来封装一个请求。要达到这一点,命令对象将动作和接收者包进对象中。这个命令对象只暴露出一个execute()方法,当此方法被调用时,接收者就会进行这些动作。从外面来看,其他对象不知道究竟哪个接收者进行了哪些动作,只知道如果调用execute()方法,请求的目的就能达到。

让我们来看看类图:

command.png

在《Head First 设计模式》一书中,提到了一个用例:在智能家电场景中,用一个遥控器控制家电的开关,比如吊扇、音响、车库门等,遥控器中的每个按键都对应着一个家电的一个动作,比如第一个按键控制吊扇的打开,第二个按键控制吊扇高速转动,第三个按键控制吊扇中速转动,第四个按键控制吊扇关闭,第五个按键控制音响的开关。还有一个单独的撤销按钮,按下后可以撤销之前的请求。
下图展示了例子中的对象与类的对应关系。

image.png

对应的代码如下:

/**
 * 厂商类:吊扇
 * CeilingFan类是一个Receiver
 */
public class CeilingFan {
    // 下面的常亮定义了CeilingFan的转速
    public static final int HIGH = 3;
    public static final int MEDIUM = 2;
    public static final int LOW = 1;
    public static final int OFF = 0;

    private String location;
    private int speed;
    public CeilingFan(String location) {
        this.location = location;
        this.speed = OFF;
    }

    public void high() {
        speed = HIGH;
        System.out.println("speed: " + speed);
    }
    public void medium() {
        speed = MEDIUM;
        System.out.println("speed: " + speed);
    }
    public void low() {
        speed = LOW;
        System.out.println("speed: " + speed);
    }
    public void off() {
        speed = OFF;
        System.out.println("speed: " + speed);
    }
    public int getSpeed() {
        return speed;
    }
}

/**
 * 命令对象接口
 * 将命令对象定义成接口,实现调用者与执行者解耦
 */
public interface ICommand {

    void execute();
    void undo();
}
/**
 * 这是一个不做事情的命令
 */
public class NoCommand implements ICommand {
    @Override
    public void execute() {
        System.out.println("NoCommand");
    }

    @Override
    public void undo() {
        System.out.println("NoCommand undo");
    }
}

public abstract class CeillingFanCommand implements ICommand{
    CeilingFan ceilingFan;
    // 增加局部状态以便追踪CeillingFan之前的转速
    int prevSpeed;

    public CeillingFanCommand(CeilingFan ceilingFan) {
        this.ceilingFan = ceilingFan;
    }

    /**
     * 将吊扇的速度设置成之前的值,达到撤销的目的
     */
    @Override
    public void undo() {
        if (prevSpeed == CeilingFan.HIGH) {
            ceilingFan.high();
        } else if (prevSpeed == CeilingFan.MEDIUM) {
            ceilingFan.medium();
        } else if (prevSpeed == CeilingFan.LOW) {
            ceilingFan.low();
        } else if (prevSpeed == CeilingFan.OFF) {
            ceilingFan.off();
        }
    }
}
/**
 * 具体命令对象:吊扇高速运转
 */
public class CeilingFanHighCommand extends CeillingFanCommand {

    public CeilingFanHighCommand(CeilingFan ceilingFan) {
        super(ceilingFan);
    }
    @Override
    public void execute() {
        // 在我们改变吊扇的速度之前,需要先将它之前的状态记录起来,以便需要撤销时使用
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.high();
    }
}
/**
 * 具体命令对象:关闭吊扇
 */
public class CeilingFanOffCommand extends CeillingFanCommand{

    public CeilingFanOffCommand(CeilingFan ceilingFan) {
        super(ceilingFan);
    }

    @Override
    public void execute() {
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.off();
    }
}

/**
 * 遥控器,它是一个调用者Invoker
 * 有7个插槽 ,
 */
public class RemoteControl {
    private ICommand[] onCommands;
    private ICommand[] offCommands;
    // 使用堆栈存放撤销命令,可以按下撤销按钮多次,撤销到很早很早以前的状态
    private Stack<ICommand> undoCommands = new Stack<>();

    public RemoteControl() {
        onCommands = new ICommand[7];
        offCommands = new ICommand[7];

        // 将每个插槽预先指定成NoCommand对象,以便确定每个插槽都有命令对象
        NoCommand noCommand = new NoCommand();
        for (int i = 0; i < 7; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        undoCommands.push(noCommand);
    }
    public void setCommands(int slot, ICommand onCommand, ICommand offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void onButtonWasPressed(int slot) {
        onCommands[slot].execute();
        undoCommands.push(onCommands[slot]);
    }
    public void offButtonWasPressed(int slot) {
        offCommands[slot].execute();
        undoCommands.push(offCommands[slot]);
    }

    public void undoButtonWasPressed() {
        undoCommands.pop().undo();
    }
}

/**
 * 这是一个客户Client
 */
public class RemoteLoaderTest {
    public static void main(String[] args) {
	// 创建接收者实例:吊扇
        CeilingFan livingRoomCeilingFan = new CeilingFan("Living Room");
	// 创建命令对象:吊扇高速运转、低俗运转、关闭。命令对象中持有对接收者实例的引用
        CeilingFanHighCommand ceilingFanHighCommand = new CeilingFanHighCommand(livingRoomCeilingFan);
        CeilingFanLowCommand ceilingFanLowCommand = new CeilingFanLowCommand(livingRoomCeilingFan);
        CeilingFanOffCommand ceilingFanOffCommand = new CeilingFanOffCommand(livingRoomCeilingFan);
        // 创建调用者实例:遥控器。调用者对象中持有对命令对象实例的引用
        RemoteControl remoteControl = new RemoteControl();
        remoteControl.setCommands(0, ceilingFanHighCommand, CeilingFanOffCommand);
        remoteControl.setCommands(1, ceilingFanLowCommand, CeilingFanOffCommand);
	// 发出请求:操作按键0
        remoteControl.onButtonWasPressed(0);
        remoteControl.offButtonWasPressed(0);
        // 撤销请求
        remoteControl.undoButtonWasPressed();
        // 发出请求:操作按键1
        remoteControl.onButtonWasPressed(1);
        remoteControl.offButtonWasPressed(1);
        // 撤销请求
        remoteControl.undoButtonWasPressed();
    }
}

上述代码中出现了NoCommand命令对象。NoCommand对象是一个空对象(Null Object)的例子。当你不想返回一个有意义的对象时,空对象就很有用。客户也可以将处理null的责任转移给空对象。

使用状态实现撤销。 因为吊扇有多个转速,所以设置了prevSpeed变量追踪吊扇的转速以便撤销。像电灯只有打开、关闭两种动作的,无需设置状态字段,撤销时直接调用动作方法即可。

设计原则

  • 封装变化
  • 多用组合,少用继承
  • 针对接口编程,不针对实现编程
  • 为交互对象之间松耦合设计而努力
  • 类应该对扩展开放,对修改关闭

二、使用宏命令

在宏命令中,用命令数组存储一大推命令。但宏命令被执行时,就一次性执行数组里的每个命令。一个宏命令被执行完,然后按下撤销按钮,那么宏内所进行的每一个命令都必须被撤销。

public class MacroCommand implements ICommand {
    // 使用数组存储多个命令
    private ICommand[] commands;

    public MacroCommand(ICommand[] commands) {
        this.commands = commands;
    }

    @Override
    public void execute() {
        for (ICommand command : commands) {
            command.execute();
        }
    }

    @Override
    public void undo() {
        for (ICommand command : commands) {
            command.undo();
        }
    }
}

三、命令模式的更多用途

1、队列请求

命令可以将运算块打包(一个接收者和一组动作),然后将它传来传去,就像是一般的对象一样。现在,即使在命令对象在创建许久之后,运算依然可以被调用。事实上,它甚至可以在不同的线程间被调用。我们可以利用这样的特性衍生一些应用。例如:日程安排、线程池、工作队列等。

想象有一个工作队列:你在某一端添加命令,然后另一端则是线程。线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令。工作队列类和进行计算的对象之间是完全解耦的

2、日志请求

某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的状态。通过新增两个方法(store()、load()),命令模式就能够支持这一点。

要怎么做呢?当我们执行命令的时候,将历史记录储存在磁盘中。一旦系统死机,我们就可以将命令对象重新加载,并成批的依次调用这些对象的execute()方法。

有许多调用大型数据结构动作的应用无法在每次改变发生时被快速的存储。通过使用记录日志,我们可以将上次检查点之后的所有操作记录下来。如果系统出状况,从检查点开始应用这些操作。这些技巧可以被扩展应用到事务处理中。


四、 总结要点

  • 命令模式将发出请求的对象和执行请求的对象解耦
  • 在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者(即执行请求的对象)和一个或一组动作。
  • 调用者通过调用命令对象的execute()发出请求,这会使得接收者(即执行请求的对象)的动作被调用。
  • 调用者可以接受命令当做参数(例如构造函数中命令对象做参数),甚至在运行时动态的进行(setCommand(ICommand command))。
  • 命令可以支持撤销,做法是实现一个undo()方法回到execute()方法被执行前的状态。
  • 宏命令是命令的一种简单的延伸,允许调用多个命令。宏命令也可以支持撤销。
  • 实际操作时,很常见使用“聪明”命令对象,也就是直接实现了请求,而不是将工作委托给接收者。
  • 命令也可以用来实现日志和事务系统

五、与其他模式的对比

命令模式与策略模式类似,但更强调操作的可撤销性与队列化(如命令历史记录),而策略模式侧重算法替换。
在Spring中,命令模式常与工厂模式结合,通过BeanFactory动态创建命令对象实例。