Head First 设计模式(六)命令模式

1. 定义

命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作

这个模式我自己感觉理解不深,敲代码时没怎么遇见过这种设计模式。所以引用JAVA设计模式(15):行为型-命令模式(Command)中的一段话来进一步解释概念:

命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。

命令模式的核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法

2. 类图

从类图中我们可以了解到,命令模式主要分为了四块:

  1. 命令(Command)
  2. 命令接受者(Receiver)
  3. 命令调用者(Invoker)
  4. 客户(Client)

整个流程可以看成,客户通过命令调用者来调用命令,而命令再调动命令接受者来处理。

如果不按照命令模式,那客户就要直接想办法调用Receiver的方法,耦合度大大增加。下面我们用场景代码来进一步说明

3. 场景+代码

场景:

现在我要设计一个遥控器类,上面有7对按钮,分别对应不同的功能(例如风扇开和风扇关、电灯开和电灯关),该如何设计?

我们先准备几个“功能类”,分别代码遥控器按钮要执行的功能:

/**
 * 电灯
 */
public class Light {
    public void on() {
        System.out.println("电灯打开了");
    }

    public void off() {
        System.out.println("电灯关闭了");
    }
}

/**
 * 车库门
 */
public class GarageDoor {
    public void on() {
        System.out.println("车库门打开了");
    }

    public void off() {
        System.out.println("车库门关闭了");
    }
}

然后我们按最简单的逻辑,直接来设计这个遥控器:

public class OldRemoteControl {
    //遥控器控制的家居-电视
    Light light;
    //遥控器控制的家居-车库门
    GarageDoor garageDoor;
    ……

    public OldRemoteControl() {
        light = new Light();
        garageDoor = new GarageDoor();
        ……
    }

    /**
     * 按下某一开按钮
     */
    public void onButtonWasPressed(int i) {
        //假设遥控器第一个按钮控制电灯
        if(i == 0){
            light.on();
        }
        //假设遥控器第二个按钮控制车库门
        else if(i == 1){
            garageDoor.on();
        }
        ……
    }
    /**
     * 按下某一关按钮
     */
    public void offButtonWasPressed(int i) {
        //假设遥控器第一个按钮控制电灯
        if(i == 0){
            light.off();
        }
        //假设遥控器第二个按钮控制车库门
        else if(i == 1){
            garageDoor.off();
        }
        ……
    }
}

这样写没什么问题。遥控器可以很正常的运行。

但是,我们很快发现:

  1. 每次遥控器按钮功能改变时,我们都需要改动代码,这违反了我们之前所说的“开闭设计原则”:类应该对拓展开发,对修改关闭。
  2. 遥控器和功能类的依赖太深,换句话说,这两个类耦合很严重。

如何改进?现在就是我们用命令模式的时刻啦~

代码

之前的定义讲过,命令模式的核心在于Command类,“命令发送者”调用“命令类”,而“命令类”调用“命令接受者”,从而降低发送者和接收者的耦合度,接下来看它的详细实现:

command

/**
 * 命令接口
 */
public interface Command {
    /**
     * 执行命令
     */
    public void execute();
}

/**
 * 电灯打开命令类
 */
public class LightOnCommand implements Command{
    /**receiver*/
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    public void execute(){
        light.on();
    }
}

/**
 * 电灯关闭命令类
 */
public class LightOffCommand implements Command{
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    public void execute(){
        light.off();
    }
}

/**
 * 车库门打开命令类
 */
public class GarageDoorOnCommand implements Command{
    private GarageDoor garageDoor;

    public GarageDoorOnCommand(GarageDoor garageDoor) {
        this.garageDoor = garageDoor;
    }

    public void execute(){
        garageDoor.on();
    }
}

/**
 * 车库门关闭命令类
 */
public class GarageDoorOffCommand implements Command{
    private GarageDoor garageDoor;

    public GarageDoorOffCommand(GarageDoor garageDoor) {
        this.garageDoor = garageDoor;
    }

    public void execute(){
        garageDoor.off();
    }
}

receiver

/**
 * 电灯
 */
public class Light {
    public void on() {
        System.out.println("电灯打开了");
    }

    public void off() {
        System.out.println("电灯关闭了");
    }
}

/**
 * 车库门
 */
public class GarageDoor {
    public void on() {
        System.out.println("车库门打开了");
    }

    public void off() {
        System.out.println("车库门关闭了");
    }
}

invoker

我们现在来看一下,加了Command后的遥控器类:

/**
 * 遥控器,有多个指令
 */
public class RemoteControl {
    /**开命令按钮*/
    Command[] onCommands;
    /**关命令按钮*/
    Command[] offCommands;
    /**空对象*/
    public static Command noCommand = new NoCommand();

    public RemoteControl() {
        //设置遥控器有7对按钮,分别对应打开、关闭
        onCommands = new Command[7];
        offCommands = new Command[7];
        //将所有按钮初始化为空按钮,反正空指针报错
        for(int i = 0 ; i < 7 ; i++){
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
    }

    /**
     * 设置一对按钮
     */
    public void setCommand(int i,Command onCommand,Command offCommand) {
        onCommands[i] = onCommand;
        offCommands[i] = offCommand;
    }

    /**
     * 按下某一开按钮
     */
    public void onButtonWasPressed(int i) {
        onCommands[i].execute();
    }
    /**
     * 按下某一关按钮
     */
    public void offButtonWasPressed(int i) {
        offCommands[i].execute();
    }
}

/**
 * 一般invoker,只对应一个Command。此处给出作demo
 */
public class SimpleRemoteControl {
    Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void buttonWasPressed() {
        command.execute();
    }
}

这段代码用到了“空对象”的概念。

我们想象一下,假如遥控器只设置了前两对按钮的功能,那为了我们按到其余按钮时程序不出错,需要进行判空操作:

public void onButtonWasPressed(int i) {
        if(onCommands[i] != null)
            onCommands[i].execute();
    }

这样做麻烦很多,我们需要谨慎的判断各种可能出现空指针的情况,另外一旦出现空指针,程序都会被中断,而且没有任何空对象的提示。

为了改善这种情况,我们定义一个空对象命令类:

/**
 * 空指令,null的取代对象
 */
public class NoCommand implements Command{

    @Override
    public void execute() {
        System.out.println("什么都不做……");
    }

    @Override
    public void undo() {
        System.out.println("什么都不做……");
    }
}

再在命令调用类(遥控器)初始化时,指定所有按钮为空命令

public RemoteControl() {
        //设置遥控器有7对按钮,分别对应打开、关闭
        onCommands = new Command[7];
        offCommands = new Command[7];
        //将所有按钮初始化为空按钮,反正空指针报错
        for(int i = 0 ; i < 7 ; i++){
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
    }

这样,便不需要再进行空判断了,而且即使按错了空功能的按钮,也会进行提示。

client

最后,由客户,也就是遥控器操纵者来发送命令,进行测试:

/**
 * 遥控器客户
 */
public class RemoteControlClient {

    public static void main(String[] args) {
        //客户创建一组命令对象,并将其放入调用者中(想要的遥控器)
        RemoteControl remoteControl = generateControl();
        //客户发送命令请求(操纵遥控器)
        remoteControl.onButtonWasPressed(0);
        System.out.println("=================");
        remoteControl.onButtonWasPressed(1);
    }

    /**
     *  创建一个遥控器
     */
    public static RemoteControl generateControl(){
        Light light = new Light();
        GarageDoor garageDoor = new GarageDoor();

        Command lightOn = new LightOnCommand(light);
        Command lightOff = new LightOffCommand(light);
        Command garageDoorOn = new GarageDoorOnCommand(garageDoor);
        Command garageDoorOff = new GarageDoorOffCommand(garageDoor);

        RemoteControl remoteControl = new RemoteControl();
        remoteControl.setCommand(0, lightOn, lightOff);
        remoteControl.setCommand(1, garageDoorOn,garageDoorOff);

        return remoteControl;
    }
}

拓展

撤销功能

现在我想要遥控器有撤销功能,即按下撤销键,把最近的一次操作撤销掉,该如何做?

首先,给每个命令类都增加一个撤销的方法:

/**
 * 命令接口
 */
public interface Command {
    /**
     * 执行命令
     */
    public void execute();
    /**
     * 撤销命令
     */
    public void undo();
}

/**
 * 电灯打开命令类
 */
public class LightOnCommand implements Command{
    /**receiver*/
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    public void execute(){
        light.on();
    }

    @Override
    public void undo() {
        light.off();
    }
}

/**
 * 电灯关闭命令类
 */
public class LightOffCommand implements Command{
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    public void execute(){
        light.off();
    }

    @Override
    public void undo() {
        light.on();
    }
}

/**
 * 车库门打开命令类
 */
public class GarageDoorOnCommand implements Command{
    private GarageDoor garageDoor;

    public GarageDoorOnCommand(GarageDoor garageDoor) {
        this.garageDoor = garageDoor;
    }

    public void execute(){
        garageDoor.on();
    }

    @Override
    public void undo() {
        garageDoor.off();
    }
}

/**
 * 车库门关闭命令类
 */
public class GarageDoorOffCommand implements Command{
    private GarageDoor garageDoor;

    public GarageDoorOffCommand(GarageDoor garageDoor) {
        this.garageDoor = garageDoor;
    }

    public void execute(){
        garageDoor.off();
    }

    @Override
    public void undo() {
        garageDoor.on();
    }
}

然后用一个变量,记录每次命令调用者(遥控器)的最后一次操作

public class RemoteControl {
    /**开命令按钮*/
    Command[] onCommands;
    /**关命令按钮*/
    Command[] offCommands;
    /**最后一次执行的命令*/
    Command lastCommand;
    /**空对象*/
    public static Command noCommand = new NoCommand();


    public RemoteControl() {
        //设置遥控器有7对按钮,分别对应打开、关闭
        onCommands = new Command[7];
        offCommands = new Command[7];
        //将所有按钮初始化为空按钮,反正空指针报错
        for(int i = 0 ; i < 7 ; i++){
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        lastCommand = noCommand;
    }

    /**
     * 设置一对按钮
     */
    public void setCommand(int i,Command onCommand,Command offCommand) {
        onCommands[i] = onCommand;
        offCommands[i] = offCommand;
    }

    /**
     * 按下某一开按钮
     */
    public void onButtonWasPressed(int i) {
        lastCommand = onCommands[i];
        if(onCommands[i] != null)
        onCommands[i].execute();
    }
    /**
     * 按下某一关按钮
     */
    public void offButtonWasPressed(int i) {
        lastCommand = offCommands[i];
        offCommands[i].execute();
    }

    /**
     *  撤销最近一次的操作
     */
    public void cancelButtonWasPressed() {
        lastCommand.undo();
    }
}

这样便成功了,如果想多次撤销之前的命令,同理,只需设置一个集合存储之前操作的所有命令,再一一调用undo()方法即可。

多功能综合按钮

看到之前的代码,可能有人会疑惑“接受者”存在的意义。为什么不直接在命令类的execute()方法中直接实现所有的逻辑呢?

之前设计的命令类,都是“简单式”的命令类。即它只懂得调用一个接受者的一个行为。当我们要实现“复杂式”的命令类,调用多个接受者行为时,“接受者”存在便很重要,它可以帮助我们解耦。看下面的例子。

现在我嫌遥控器按钮功能太单一,例如我想要按一个按钮同时开灯和开车库门,如何设计这种“批处理”按钮?

设计一个“批处理命令类”

/**
 * 复杂命令,由多个命令组成
 */
public class MacroCommand implements Command{
    private Command[] commands;

    public MacroCommand(Command[] commands) {
        this.commands = commands;
    }
    @Override
    public void execute(){
        for (Command command : commands) {
            command.execute();
        }
    }

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

将批处理命令与按钮绑定,测试

/**
 * 遥控器客户
 */
public class RemoteControlClient {

    public static void main(String[] args) {
        //客户创建一组命令对象,并将其放入调用者中(想要的遥控器)
        RemoteControl remoteControl = generateControl();
        //客户发送命令请求(操纵遥控器)
        System.out.println("========测试开灯=========");
        remoteControl.onButtonWasPressed(0);
        System.out.println("========测试开车库门=========");
        remoteControl.onButtonWasPressed(1);
        System.out.println("=========测试综合按钮========");
        remoteControl.onButtonWasPressed(2);
        System.out.println("=========测试撤销按钮========");
        remoteControl.cancelButtonWasPressed();
    }

    /**
     *  创建一个遥控器
     */
    public static RemoteControl generateControl(){
        Light light = new Light();
        GarageDoor garageDoor = new GarageDoor();
        //创建“傻瓜式”命令
        Command lightOn = new LightOnCommand(light);
        Command lightOff = new LightOffCommand(light);
        Command garageDoorOn = new GarageDoorOnCommand(garageDoor);
        Command garageDoorOff = new GarageDoorOffCommand(garageDoor);
        //创建“批处理”命令
        Command[] maxOn = new Command[]{lightOn,garageDoorOn};
        Command[] maxOff = new Command[]{lightOff,garageDoorOff};
        MacroCommand partyOn =  new MacroCommand(maxOn);
        MacroCommand partyOff =  new MacroCommand(maxOff);
        //将命令与调用者绑定
        RemoteControl remoteControl = new RemoteControl();
        remoteControl.setCommand(0, lightOn, lightOff);
        remoteControl.setCommand(1, garageDoorOn,garageDoorOff);
        remoteControl.setCommand(2, partyOn,partyOff);

        return remoteControl;
    }
}/**Output:
========测试开灯=========
电灯打开了
========测试开车库门=========
车库门打开了
=========测试综合按钮========
电灯打开了
车库门打开了
=========测试撤销按钮========
电灯关闭了
车库门关闭了*/

3. 用途

命令模式运用于:线程池、工作队列和日志请求等等


本文总结自
《Head First 设计模式》第六章:命令模式

部分参考于:
JAVA设计模式(15):行为型-命令模式(Command)

猜你喜欢

转载自blog.csdn.net/z55887/article/details/70136083