bboyjing's blog

HeadFirst设计模式六【命令模式】

命令模式把封装带到一个全新的境界,把方法调用封装起来。乍一听好像没什么特别的,不明白这个模式有什么特别的地方,下面就来详细看下。

场景描述

假设有一个遥控器,上面有N个插槽,插槽中可以插入接口匹配的电器,比如电灯、电风扇之类。每一个插槽对应两个开关:ON、OFF,外加一个整体共用的撤销按钮,会撤销最后一个按钮的动作。这个场景的关键问题在于遥控器并不知道每个电器的内部实现,但是插上去却能控制电器的开关 ,遥控器与电器完全解耦。下面就看下命令模式是如何实现的。

代码示例

下面将由浅入深,来逐步实现命令模式。

示例一:简化场景

把场景简化下,假设遥控器上只有一个按钮,同时只提供一个插槽,我们有一个电灯需要接上去。

  1. 先定义一个电灯,很简单,就提供一个开灯的方法。

    1
    2
    3
    4
    5
    public class Light {
    public void on() {
    System.out.println("Light is on");
    }
    }
  2. 定义命令接口,并且提供一个开灯命令的实现。实现的构造函数中被传入了某个电灯,以便让命令控制。调用命令的execute()其实就是在调用命令请求接收者(此处就是Light对象)的on()方法。

    1
    2
    3
    public interface Command {
    public void execute();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class LightOnCommand implements Command {
    /**
    * 请求接收者
    */
    Light light;
    public LightOnCommand(Light light) {
    this.light = light;
    }
    @Override
    public void execute() {
    light.on();
    }
    }
  3. 定义简单的遥控器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class SimpleRemoteControl {
    /**
    * 插槽,持有一个命令接口
    */
    Command slot;
    /**
    * 设置插槽持有的命令
    *
    * @param command
    */
    public void setCommand(Command command) {
    slot = command;
    }
    /**
    * 按下按钮动作
    */
    public void buttonWasPressed() {
    slot.execute();
    }
    }
  4. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Test {
    public static void main(String[] args) {
    SimpleRemoteControl remote = new SimpleRemoteControl();
    Light light = new Light();
    LightOnCommand lightOn = new LightOnCommand(light);
    remote.setCommand(lightOn);
    remote.buttonWasPressed();
    }
    }

这个简化的场景下,感觉Command接口可以省略,比如换成一个Electric接口,定义一个on()方法,电灯实现该接口,SimpleRemoteControl中直接注入Electric也可以实现。好像没命令那一层什么事儿,反而更简单了。可能是简化场景的错觉,又或者是没有真正理解命令模式,继续深入看看。

示例二:完善7个插槽的遥控器

这个例子我们把遥控器的7个插槽完善,并且多加一个音响电器试试。

  1. 定义电灯、音响类,需要注意的是Stereo添加了额外的方法,因为音响开启的方式跟电灯不一样。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Light {
    /**
    * 电灯名称,用于区分是哪个房间的电灯
    */
    private String name;
    public Light(String name) {
    this.name = name;
    }
    public void on() {
    System.out.println(name + " light is on");
    }
    public void off() {
    System.out.println(name + " light is off");
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Stereo {
    public void on() {
    System.out.println("Stereo on");
    }
    public void off() {
    System.out.println("Stereo off");
    }
    public void setCD() {
    System.out.println("Stereo set CD");
    }
    public void setVolume(int volume) {
    System.out.println("Stereo set volume " + volume);
    }
    }
  2. 定义相关命令,这里我们定义了一组电灯开关命令和一组音响开关命令

    1
    2
    3
    public interface Command {
    public void execute();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class LightOnCommand implements Command {
    /**
    * 请求接收者
    */
    Light light;
    public LightOnCommand(Light light) {
    this.light = light;
    }
    @Override
    public void execute() {
    light.on();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class LightOffCommand implements Command {
    /**
    * 请求接收者
    */
    Light light;
    public LightOffCommand(Light light) {
    this.light = light;
    }
    @Override
    public void execute() {
    light.off();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class StereoOnWithCDCommand implements Command {
    Stereo stereo;
    public StereoOnWithCDCommand(Stereo stereo) {
    this.stereo = stereo;
    }
    @Override
    public void execute() {
    stereo.on();
    stereo.setCD();
    stereo.setVolume(111);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class StereoOffCommand implements Command {
    Stereo stereo;
    public StereoOffCommand(Stereo stereo) {
    this.stereo = stereo;
    }
    @Override
    public void execute() {
    stereo.off();
    }
    }
  3. 定义遥控器,完善7个插槽

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class RemoteControl {
    Command[] onCommands;
    Command[] offCommands;
    public RemoteControl() {
    this.onCommands = new Command[7];
    this.offCommands = new Command[7];
    }
    public void setCommand(int slot, Command onCommand, Command offCommand) {
    onCommands[slot] = onCommand;
    offCommands[slot] = offCommand;
    }
    public void onButtonWasPressed(int slot) {
    onCommands[slot].execute();
    }
    public void offButtonWasPressed(int slot) {
    offCommands[slot].execute();
    }
    }
  4. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class Test {
    public static void main(String[] args) {
    RemoteControl remoteControl = new RemoteControl();
    // 电器类
    Light livingRoomLight = new Light("Living Room");
    Light kitchenLight = new Light("Kitchen");
    Stereo stereo = new Stereo();
    // 命令类
    LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
    LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
    LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
    LightOffCommand kichenLightOff = new LightOffCommand(kitchenLight);
    StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo);
    StereoOffCommand stereoOff = new StereoOffCommand(stereo);
    // 遥控器
    remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
    remoteControl.setCommand(1, kitchenLightOn, kichenLightOff);
    remoteControl.setCommand(2, stereoOnWithCD, stereoOff);
    // 测试开关
    remoteControl.onButtonWasPressed(0);
    remoteControl.offButtonWasPressed(0);
    remoteControl.onButtonWasPressed(1);
    remoteControl.offButtonWasPressed(1);
    remoteControl.onButtonWasPressed(2);
    remoteControl.offButtonWasPressed(2);
    }
    }

在这个场景中,体会到命令模式的好处了。因为我们添加的音响有一个比较复杂的开启过程,其他电器可能更复杂,无法抽象出一个统一的行为。使用命令模式的话,遥控器只要知道插入插槽的命令是什么,执行execute()方法就可以了,而不必知道每个电器开、关的细节。

示例三:最后一步,加上撤销功能

先来讲下撤销功能的作用:比如收客厅的电灯是关闭的,然后按下遥控器上的开启按钮,电灯就被打开了。现在如果按下撤销按钮,那么上一个动作将被倒转。在这个例子中,电灯将被关闭。下面同样以简单的方式实现。

  1. 定义电灯

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Light {
    public void on() {
    System.out.println("Light is on");
    }
    public void off() {
    System.out.println("Light is off");
    }
    }
  2. 定义命令接口,以及电灯开、关命令的实现。这次在接口中添加了一个undo方法,用于撤销时执行的操作。

    1
    2
    3
    4
    5
    6
    7
    8
    public interface Command {
    public void execute();
    /**
    * 给命令接口添加undo方法
    */
    public void undo();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class LightOnCommand implements Command {
    Light light;
    public LightOnCommand(Light light) {
    this.light = light;
    }
    @Override
    public void execute() {
    light.on();
    }
    /**
    * execute()是打开电灯,所以undo()该做事情就是关闭电灯
    */
    @Override
    public void undo() {
    light.off();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class LightOffCommand implements Command {
    Light light;
    public LightOffCommand(Light light) {
    this.light = light;
    }
    @Override
    public void execute() {
    light.off();
    }
    /**
    * undo()把电灯打开
    */
    @Override
    public void undo() {
    light.on();
    }
    }
  3. 定义支持撤销按钮的遥控器。在这里添加了undoCommand,用于记录最后一次执行的操作是什么。具体undo的逻辑由各个命令实现,遥控器不关注。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    public class RemoteControlWithUndo {
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand;
    public RemoteControlWithUndo() {
    this.onCommands = new Command[7];
    this.offCommands = new Command[7];
    }
    public void setCommand(int slot, Command onCommand, Command offCommand) {
    onCommands[slot] = onCommand;
    offCommands[slot] = offCommand;
    }
    /**
    * 按下按钮时先执行命令
    * 然后将该命令记录在undoCommand变量中
    *
    * @param slot
    */
    public void onButtonWasPressed(int slot) {
    onCommands[slot].execute();
    undoCommand = onCommands[slot];
    }
    public void offButtonWasPressed(int slot) {
    offCommands[slot].execute();
    undoCommand = offCommands[slot];
    }
    public void undoButtonWasPressed() {
    undoCommand.undo();
    }
    }
  4. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Test {
    public static void main(String[] args) {
    RemoteControlWithUndo remoteControlWithUndo = new RemoteControlWithUndo();
    Light light = new Light();
    LightOnCommand lightOnCommand = new LightOnCommand(light);
    LightOffCommand lightOffCommand = new LightOffCommand(light);
    remoteControlWithUndo.setCommand(0, lightOnCommand, lightOffCommand);
    remoteControlWithUndo.onButtonWasPressed(0);
    remoteControlWithUndo.offButtonWasPressed(0);
    remoteControlWithUndo.undoButtonWasPressed();
    }
    }

命令模式到这里就学完了,这个模式下明显的感受是尽量解耦,让一个类的功能尽量单一,比如说控制电灯的命令抽象成开、关两个命令,而不是一个集合了开、关的命令。当然这只是个人主观的感受,仅供参考。