设计模式之命令模式

情景:

屋里有很多家用电器,你需要设计一个遥控器,来控制所有电器的使用。

如果在遥控器中添加电器类,那就使得遥控器和具体电器类过度耦合了,遥控器不应该知道电器的实现细节。

遥控器应该简单一些,我们都知道遥控器只要一些按钮,所能做的动作仅仅是按下按钮,所以不应该包含太多的控制逻辑。

所以,这里需要用命令模式,来将“动作的请求者”从“动作的执行者”对象中解耦。

设计一个命令对象,遥控器可以执行命令对象,而不关心命令具体是做什么。

在命令对象内部,有具体的电器类和命令的具体实现,以及公开给遥控器的执行方法。

 

一个简单的实现:

首先是一个命令接口,所有的命令都要实现这个接口,然后放到遥控器上,而遥控器只要用execute()执行就好了,不需要知道命令细节。

public interface Command {
    public void execute();
}

一个具体的命令,开灯命令。

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

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

灯:

public class Light {
    public void on() {
        System.out.println("灯亮了");
    }
    
    public void off() {
        System.out.println("灯灭了");
    }
}

遥控器:遥控器上有插槽用来持有命令。(可以考虑成遥控器上一个按钮-_-#)

public class SimpleRemoteControl {
    Command slot;
    
    public SimpleRemoteControl() {}
    
    public void setCommand(Command command) {
        slot = command;
    }
    
    public void buttonWasPressed() {
        slot.execute();
    }
}

测试类:

public class RemoteControlTest {
    public static void main(String[] args) {
        SimpleRemoteControl remote = new SimpleRemoteControl();
        Light light = new Light();
        LightOnCommand lightOn = new LightOnCommand(light);
        
        remote.setCommand(lightOn);
        remote.buttonWasPressed();
    }
}

输出: 灯亮了 

 

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

类图:

命令模式类图

 

上面的遥控器只有一个命令,事实上有很多的电器需要控制,而且不仅需要开命令,也需要有关闭命令。

实现遥控器类:

有七个开启按钮和七个关闭按钮。

public class RemoteControl {
    Command[] onCommands;
    Command[] offCommands;
    
    public RemoteControl() {
        onCommands = new Command[7];
        offCommands = new Command[7];
        
        Command noCommand = new NoCommand();
        for (int i = 0; i < 7; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
    }
    
    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }
    
    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
    }
    
    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
    }
    
    public String toString() {
        StringBuffer stringBuff = new StringBuffer();
        stringBuff.append("\n----- Remote Control -----\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuff.append("[slot" + i + "] " + onCommands[i].getClass().getName() + 
                    "\t\t" + offCommands[i].getClass().getName() + "\n");
        }
        return stringBuff.toString();
    }
}

具体命令(太多了,不贴啦):

public class StereoOffWithCDCommand implements Command {
    Stereo stereo;
    
    public StereoOffWithCDCommand(Stereo stereo) {
        this.stereo = stereo;
    }
    
    public void execute() {
        stereo.off();
        stereo.popCD();
    }
}

测试一下:

public class RemoteLoader {
    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();
        
        Light light = new Light();
        GarageDoor garageDoor = new GarageDoor();
        Stereo stereo = new Stereo();
        
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        
        GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
        GarageDoorCloseCommand garageDoorCloseCommand = new GarageDoorCloseCommand(garageDoor);
        
        StereoOnWithCDCommand stereoOnWithCDCommand = new StereoOnWithCDCommand(stereo);
        StereoOffWithCDCommand stereoOffWithCDCommand = new StereoOffWithCDCommand(stereo);
        
        remoteControl.setCommand(0, lightOnCommand, lightOffCommand);
        remoteControl.setCommand(1, garageDoorOpenCommand, garageDoorCloseCommand);
        remoteControl.setCommand(2, stereoOnWithCDCommand, stereoOffWithCDCommand);
        
        System.out.println(remoteControl);
        
        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        remoteControl.onButtonWasPushed(1);
        remoteControl.offButtonWasPushed(1);
        remoteControl.onButtonWasPushed(2);
        remoteControl.offButtonWasPushed(2);
        
    }
}

输出:

----- Remote Control -----
[slot0] com.wenr.chapter6.LightOnCommand        com.wenr.chapter6.LightOffCommand
[slot1] com.wenr.chapter6.GarageDoorOpenCommand        com.wenr.chapter6.GarageDoorCloseCommand
[slot2] com.wenr.chapter6.StereoOnWithCDCommand        com.wenr.chapter6.StereoOffWithCDCommand
[slot3] com.wenr.chapter6.NoCommand        com.wenr.chapter6.NoCommand
[slot4] com.wenr.chapter6.NoCommand        com.wenr.chapter6.NoCommand
[slot5] com.wenr.chapter6.NoCommand        com.wenr.chapter6.NoCommand
[slot6] com.wenr.chapter6.NoCommand        com.wenr.chapter6.NoCommand

灯亮了
灯灭了
车库门开了
车库门关了
音响已打开
在音响中放入CD
音响声音调到11
音响已关闭
在音响中取出CD

 

重点来了,敲黑板!

空对象模式:

有些按钮还有没有被分配命令,如果我们将其赋值为null的话,onButtonWasPressed()就要这样写:

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

为了减少判断的麻烦,可以为其付一个空命令,它是一个不做任何事情的对象,是一个空对象(null object)。

当你不想返回一个有意义的对象时,空对象就很有用。客户也可以将处理null的责任转移给空对象。

 

到目前为止,我们还有一个要求没有实现,那就是撤销功能,撤销上一条命令。

首先,修改Command接口:

public interface Command {
    public void execute();
    public void undo();
}

然后修改遥控器类,很简单,只需要记录一下上一条命令就可以了。

public class RemoteControlWithUndo {
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand;    // 记录前一个命令
    
    public RemoteControlWithUndo() {
        onCommands = new Command[7];
        offCommands = new Command[7];
        
        Command noCommand = new NoCommand();
        for (int i = 0; i < 7; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        undoCommand = noCommand;
    }
    
    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }
    
    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
        undoCommand = onCommands[slot];
    }
    
    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }

    public void undoButtonWasPushed() {
        undoCommand.undo();
    }
}

再次测试一下:

public class RemoteLoader {
    public static void main(String[] args) {
        RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
        
        Light light = new Light();
        GarageDoor garageDoor = new GarageDoor();
        Stereo stereo = new Stereo();
        
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        
        GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
        GarageDoorCloseCommand garageDoorCloseCommand = new GarageDoorCloseCommand(garageDoor);
        
        StereoOnWithCDCommand stereoOnWithCDCommand = new StereoOnWithCDCommand(stereo);
        StereoOffWithCDCommand stereoOffWithCDCommand = new StereoOffWithCDCommand(stereo);
        
        remoteControl.setCommand(0, lightOnCommand, lightOffCommand);
        remoteControl.setCommand(1, garageDoorOpenCommand, garageDoorCloseCommand);
        remoteControl.setCommand(2, stereoOnWithCDCommand, stereoOffWithCDCommand);
        
        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        remoteControl.undoButtonWasPushed();
        remoteControl.onButtonWasPushed(1);
        remoteControl.offButtonWasPushed(1);
        remoteControl.undoButtonWasPushed();
        
    }
}

输出:

灯亮了
灯灭了
灯亮了
车库门开了
车库门关了
车库门开了

 

很好,基本功能都实现了。

现在的遥控器只能撤销前一条命令,如果想要连续撤销,可以用一个栈来保存运行过的命令。

 

新的需求来了,现在希望按下一个按钮可以做许多事情,嗯……比如你从外面回家了,你想开灯,打开电视,打开音响……你想睡觉了,你要关灯关电视关音响。有没有办法,用一个按钮做一系列事情?

定义一个宏命令:

public class MacroCommand implements Command {
    Command[] commands;
    
    public MacroCommand(Command[] commands) {
        this.commands = commands; 
    }
    
    public void execute() {
        for (int i = 0; i < commands.length; i++) {
            commands[i].execute();
        }
    }
    
    public void undo() {
        for (int i = 0; i < commands.length; i++) {
            commands[i].undo();
        }
    }
}

测试:

public class RemoteLoader3 {
    public static void main(String[] args) {
        RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
        
        Light light = new Light();
        GarageDoor garageDoor = new GarageDoor();
        Stereo stereo = new Stereo();
        
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        
        StereoOnWithCDCommand stereoOnWithCDCommand = new StereoOnWithCDCommand(stereo);
        StereoOffWithCDCommand stereoOffWithCDCommand = new StereoOffWithCDCommand(stereo);
        
        Command[] onCommands = {lightOnCommand, stereoOnWithCDCommand};
        Command[] offCommands = {lightOffCommand, stereoOffWithCDCommand};
        
        MacroCommand onMacroCommand = new MacroCommand(onCommands); 
        MacroCommand offMacroCommand = new MacroCommand(offCommands); 
        
        remoteControl.setCommand(0, onMacroCommand, offMacroCommand);
        
        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        remoteControl.undoButtonWasPushed();
    }
}

 

命令可以将运算块打包(一个接收者和一组动作),然后将它传来传去,就像是一般的对象一样。

现在,即使在命令对象被创建许久之后,运算依然可以被调用。

事实上,它甚至可以在不同的线程中被调用。

我们可以利用这样的特性衍生一些应用

例如:“日程安排” “线程池” “工作队列” 等

工作队列和进行计算的对象之间完全是解耦的。

 

通过新增两个方法store()和load(),我们可以将所有的动作都记录在日志中,,并能在系统死机后,重新调用这些动作恢复到之前的状态。

 interface Command { execute(); undo(); store(); load(); } 

 

posted @ 2017-03-13 16:29  我不吃饼干呀  阅读(216)  评论(0编辑  收藏  举报