《HeadFirst设计模式》读书笔记-第6章-命令模式

定义

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

这里写图片描述

从上面的类图可以看出,一个命令对象通过在特定接收者上绑定一组动作来封装一个请求。要达到这一点,命令对象将动作和接收者包装进对象中,这个对象只暴露出execute()/undo()方法,当execute()/undo()方法被调用的时候,接收者就会执行这些动作。站在客户的角度来看,它只要把接收者封装到命令对象,通过把命令对象设置到调用者,后续就由调用者去触发命令执行。把不同的命令对象作为参数传递给调用者,就会触发执行不同的命令。

代码实现

事实上每次接触到一个新的设计模式时,看到定义中的类图总觉得非常抽象,所以每次我都是先看具体的例子的代码实现,反过来再看定义中的类图的。例子的代码更加具体,直接,定义中的类图非常的抽象,通用,所以例子这部分是我组织文章的重点。

废话不多说了,本例要解决的问题是,遥控器的问题,遥控器有7个插槽,每个插槽有开/关两个按钮,每个插槽接上不同的家电就可以控制不同的家电,还有一个撤销按钮用来撤销上次的动作。由于不同的家电,给的控制API是不同的,该如何设计?

这里写图片描述

上面是类图,首先看Command接口的定义,它定义了2个方法,

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

再看接收者(这里是不同的家电)给的API,

// 灯
public class Light {
    String location; //灯的位置
    int level; // 灯的亮度级别

    public Light(String location) {
        this.location = location;
    }

    public void on() {
        level = 100;
        System.out.println("Light is on");
    }

    public void off() {
        level = 0;
        System.out.println("Light is off");
    }

    public void dim(int level) {
        this.level = level;
        if (level == 0) {
            off();
        }
        else {
            System.out.println("Light is dimmed to " + level + "%");
        }
    }

    public int getLevel() {
        return level;
    }
}
// 天花板的风扇
public class 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;
    String location; //风扇的位置
    int speed; // 风扇的风力等级

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

    public void high() {
        speed = HIGH;
        System.out.println(location + " ceiling fan is on high");
    } 

    public void medium() {
        speed = MEDIUM;
        System.out.println(location + " ceiling fan is on medium");
    }

    public void low() {
        speed = LOW;
        System.out.println(location + " ceiling fan is on low");
    }

    public void off() {
        speed = OFF;
        System.out.println(location + " ceiling fan is off");
    }

    public int getSpeed() {
        return speed;
    }
}

下面是具体的命令对象,它封装了家电及控制家电的动作。

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

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

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

    public void undo() {
        light.on();
    }
}
// 开灯命令
public class LightOnCommand implements Command {
    Light light;

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

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

    public void undo() {
        light.off();
    }
}
// 开高档风扇命令
public class CeilingFanHighCommand implements Command {
    CeilingFan ceilingFan;
    int prevSpeed;

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

    public void execute() {
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.high();
    }

    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 CeilingFanMediumCommand implements Command {
    CeilingFan ceilingFan;
    int prevSpeed;

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

    public void execute() {
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.medium();
    }

    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 CeilingFanLowCommand implements Command {
    CeilingFan ceilingFan;
    int prevSpeed;

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

    public void execute() {
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.low();
    }

    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 CeilingFanOffCommand implements Command {
    CeilingFan ceilingFan;
    int prevSpeed;

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

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

    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();
        }
    }
}

NoCommand对象是个null object,用来初始化调用者,这样可保证调用者已经持有了命令对象,要么是NoCommand,什么都不做,要么是具体的命令对象,可以避免去做是否为空的判断。

public class NoCommand implements Command {
    public void execute() { }
    public void undo() { }
}

调用者对象,

import java.util.*;

public class RemoteControlWithUndo {
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand;

    public RemoteControlWithUndo() {
        // 对应7个开关按钮
        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;
    }

    // 设置第slot个插槽的开/关命令对象
    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void onButtonWasPushed(int slot) {
         // 这里可以避免去做onCommands[slot]是否为空的判断
        onCommands[slot].execute();
        undoCommand = onCommands[slot];
    }

    public void offButtonWasPushed(int slot) {
        // 这里可以避免去做offCommands[slot]是否为空的判断
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }

    public void undoButtonWasPushed() {
        // 这里可以避免去做undoCommand是否为空的判断
        undoCommand.undo();
    }

    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()
                + "    " + offCommands[i].getClass().getName() + "\n");
        }
        stringBuff.append("[undo] " + undoCommand.getClass().getName() + "\n");
        return stringBuff.toString();
    }
}

客户代码部分,也是测试启动代码的实现。

public class RemoteLoader {

    public static void main(String[] args) {
        // 调用者对象
        RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
        // 接收者对象,起居室的灯
        Light livingRoomLight = new Light("Living Room");
        // 命令对象
        LightOnCommand livingRoomLightOn = 
                new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = 
                new LightOffCommand(livingRoomLight);
        // 安装插槽0 命令对象
        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);

        // 触发插槽0对应的命令对象执行
        remoteControl.onButtonWasPushed(0); // 开起居室的灯
        remoteControl.offButtonWasPushed(0); // 关起居室的灯
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed(); // 执行撤销命令
        remoteControl.offButtonWasPushed(0);
        remoteControl.onButtonWasPushed(0);
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed();

        // 接收者对象,起居室中天花板吊扇
        CeilingFan ceilingFan = new CeilingFan("Living Room");

        CeilingFanMediumCommand ceilingFanMedium = 
                new CeilingFanMediumCommand(ceilingFan);
        CeilingFanHighCommand ceilingFanHigh = 
                new CeilingFanHighCommand(ceilingFan);
        CeilingFanOffCommand ceilingFanOff = 
                new CeilingFanOffCommand(ceilingFan);

        remoteControl.setCommand(0, ceilingFanMedium, ceilingFanOff);
        remoteControl.setCommand(1, ceilingFanHigh, ceilingFanOff);

        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed();

        remoteControl.onButtonWasPushed(1);
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed();
    }
}

从上面的启动代码可以看出,给插槽安装不同的命令对象,就会触发不同的命令执行,而命令对象又有封装接收者及要执行的命令。这样遥控器就跟具体的家电解耦了,运行时安装不同了命令对象即可。

基于基本的命令模式,可做更多的扩展和应用:

  1. 实现宏命令

    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();
        }
    }
    }
    

    这样客户就可以自定义命令组了,而且这个宏命令包含的命令组还可以运行时动态调整的,非常灵活。

        Command[] partyOn = { lightOn, stereoOn, tvOn, hottubOn};
        Command[] partyOff = { lightOff, stereoOff, tvOff, hottubOff};
    
        MacroCommand partyOnMacro = new MacroCommand(partyOn);
        MacroCommand partyOffMacro = new MacroCommand(partyOff);
    
        remoteControl.setCommand(0, partyOnMacro, partyOffMacro);
        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
    
  2. 实现队列请求

    因为命令对象封装了一个接收者和一个(或者组)动作,把命令对象作为参数传递,可以衍生出很多应用,如日程安排(scheduler),线程池(threadpool),工作队列(workqueue)等。

    想象有一个工作队列,你在某一端添加命令,然后在另外一端则是线程。线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,执行完这个命令后丢弃,循环执行前面都动作。

  3. 实现日志请求

    某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的动作。通过新增两个方法(store和load),命令模式就能支持这一点,在系统正常执行命令时,将每个命令都store在磁盘中,一旦系统死机重启后,将之前store的命令load出来,重新调用execute()即可。

    public interface Command {
        public void execute();
        public void undo();
        public void store(Command[] cmd);
        public void load();
    }

该模式体现了哪些OO原则

  1. 原则2: 针对接口编程,而不是针对实现编程

    RemoteControlWithUndo的onButtonWasPushed/offButtonWasPushed方法的实现就符合该原则。

本章总结

  1. 命令模式将发出请求的对象和执行请求的对象解耦。

  2. 在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个(或者组)动作。

  3. 实际操作时,常使用“聪明”命令对象,也就是命令对象直接实现了请求,而不是将工作委托给接收者。

  4. 命令也可以用来实现日志和事务系统。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值