文章目录
通过命令模式,可以把方法封装起来,因此调用此方法的对象就不需要关心方法是如何完成的,只需要知道如何使用包装成形的方法来完成它就可以
1 模拟家电遥控器
1.1 家电遥控器的需求
给一个上述形状的遥控器,左边的插槽可以插拔家电控制块,右边的按键可以可以控制对应家电的开关,我们需要设计一组控制遥控器的API,让每个按键都能控制一个家电,还有一个撤销功能,可以撤销最后一次进行的操作
厂商提供的各种家电API:
1.2 粗糙的实现遥控器
我们对每一种家电创建一个类,先尝试实现控制一个家电的遥控器
插槽和家电:
/**
* @author 雫
* @date 2021/3/4 - 14:16
* @function 插槽
*/
public class Slot {
private String name;
public Slot(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/**
* @author 雫
* @date 2021/3/4 - 14:17
* @function 灯
*/
public class Light {
public Light() {
}
public void on() {
System.out.println("灯亮");
}
public void off() {
System.out.println("灯灭");
}
}
遥控器:
/**
* @author 雫
* @date 2021/3/4 - 14:15
* @function 遥控器
*/
public class Remoter {
private Slot slot;
private Light light;
public Remoter(Slot slot) {
this.slot = slot;
light = new Light();
}
public void buttonOnPress() {
if(slot.getName().equals("light")) {
light.on();
}
}
public void buttonOffPress() {
if(slot.getName().equals("light")) {
light.off();
}
}
}
测试:
这样粗糙的实现,违背了:
1,针对接口编程,而不是针对实现编程,代码的功能被写死,为了实现功能而写代码
2,开放关闭原则,如果要增加新的插槽或替换某个插槽控制的家电,就需要对源码进行大量的修改
3,依赖倒置原则,这里的Remoter将来会存在大量的对象依赖,实现功能与创建对象被捆绑到了一起,Remoter依赖大量的具体家电类
![](/qrcode.jpg)
为了遵循这些设计原则,设计出干净,可扩展性强,松耦合的代码,我们需要一种新的思考方式:
上面的Remoter必须实例化一些列对象(Light,TV…)才能调用这些对象的方法来操作具体的家电,但是这样就让Remoter内充满了对象,我们应该让遥控器知道什么按键被按下了,去发出一个正确的请求,由这个请求来实现具体的功能(开关灯),而不是遥控器来实现功能
即我们现在需要创建一个 “命令对象”,利用命令对象来与正确的对象(家电)沟通,让按键控制命令对象,而不是具体家电,这样就能让遥控器内的代码保持简洁
1.3 看看餐厅是如何工作的
1,客户点单,将需要的产品记录在订单里
2,服务员将订单交给厨师
3,厨师根据订单制作客户需要的产品
我们再来看上面一个点单过程涉及到的对象:
1,客户:客户将需要的产品“封装”到了订单里
订单里封装的是一些方法createBurger(),createHotDog()...
客户只需要生成订单即可,和厨师没有关系
客户只需要把订单递给服务员
2,服务员:将订单搬运到厨师
服务员不关心订单的内容是什么,或者创建订单的客户是谁
只负责将订单送给厨师,订单里包含了封装的方法
3,厨师:等待订单,一旦有订单传来,解析订单
厨师开始执行订单内被封装的方法
厨师和服务员之间没有关系,实现了解耦,他们不需要直接沟通
厨师只负责制作具体的产品
1.4 采用命令对象实现遥控器
具体家电:
/**
* @author 雫
* @date 2021/3/4 - 15:29
* @function 电灯
*/
public class Light {
public Light() {
}
public void on() {
System.out.println("打开电灯");
}
public void off() {
System.out.println("关闭电灯");
}
}
/**
* @author 雫
* @date 2021/3/4 - 15:32
* @function 电视
*/
public class TV {
public TV() {
}
public void on() {
System.out.println("打开电视");
}
public void off() {
System.out.println("关闭电视");
}
}
Command接口及它的实现类:
/**
* @author 雫
* @date 2021/3/4 - 15:28
* @function 命令接口
*/
public interface Command {
void execute();
}
/**
* @author 雫
* @date 2021/3/4 - 15:29
* @function 开灯命令
*/
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
this.light.on();
}
}
/**
* @author 雫
* @date 2021/3/4 - 15:30
* @function 关灯命令
*/
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
this.light.off();
}
}
...
Remote:
/**
* @author 雫
* @date 2021/3/4 - 15:34
* @function 简单遥控器
*/
public class SimpleRemote {
private Command command;
public SimpleRemote() {
}
public void setCommand(Command command) {
this.command = command;
}
public void buttonPressed() {
command.execute();
}
}
测试:
在上述的程序中,我们委托了命令对象来执行对家电的操作,从而实现了解耦
通过组合将实现了Command接口的类的对象作为Remote的成员,使用setCommand方法从而可以获取不同的命令对象,从而调用它们的方法来控制接收者
上述程序中:
Remote:调用者
Command:命令接口
xxxCommand:命令对象
Light:接收者
1.5 命令模式
命令模式:
将“请求”封装成对象,以便使用不同的请求或日志来参数化其它对象,命令模式也支持可撤销的操作
即调用实现了同一接口的类中的方法,而不是实例化对象后通过对象来调用方法
调用者不在乎自己拥有的命令对象是谁,只要该命令对象实现了Commad接口,它就可以间接地调用命令对象中的方法,从而控制接收者的行为
通过命令模式,我们成功实现了解耦,减少对象依赖,实现开闭原则,让系统变得可扩展,有弹性
1.6 继续实现遥控器
懂了命令模式的工作原理后,我们再更新一下遥控器的设计:
命令模式变得一目了然,调用者通过命令对象控制接收者的行为
现在我们继续实现遥控器,让遥控器的每个插槽,对应到一个接收者(Light,TV),而遥控器的ON,OFF按键,分别对应到一个命令对象,每当按下按键,相应命令对象的excute()方法就会被调用,接收者的行为就会发生变化
Remote:
/**
* @author 雫
* @date 2021/3/4 - 15:34
* @function 遥控器
*/
public class Remote {
private Command[] onCommands;
private Command[] offCommands;
/*初始化remote对象时,为每个插槽对应的开关键装上空的命令对象*/
public Remote() {
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 setOneCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot]= offCommand;
}
/*指定插槽的开键被按下,调用对应命令对象的execute方法*/
public void onButtonPressed(int slot) {
onCommands[slot].execute();
System.out.println(slot + "号插槽的开键被按下");
}
/*指定插槽的关键被按下,调用对应命令对象的execute方法*/
public void offButtonPressed(int slot) {
offCommands[slot].execute();
System.out.println(slot + "号插槽的关键被按下");
}
}
测试代码:
在初始化Remote类对象时,我们用NoCommand填满了所有命令对象的位置
这样做是为了避免空指针异常
1.7 为遥控器添加撤销功能
所谓的撤销,即执行与上一次相反的操作,我们需要在每个命令对象中新增一个undo()方法执行该命令对象excute()相反的方法,且还需要在Remote内新增一个变量记录执行最后一次操作的命令对象
Command接口和命令类:
/**
* @author 雫
* @date 2021/3/4 - 15:28
* @function 命令接口
*/
public interface Command {
void execute();
void undo();
}
/**
* @author 雫
* @date 2021/3/4 - 15:29
* @function 开灯命令
*/
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
this.light.on();
}
@Override
public void undo() {
this.light.off();
}
}
/**
* @author 雫
* @date 2021/3/4 - 15:30
* @function 关灯命令
*/
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
this.light.off();
}
@Override
public void undo() {
this.light.on();
}
}
Remote:
/**
* @author 雫
* @date 2021/3/4 - 15:34
* @function 遥控器
*/
public class Remote {
private Command[] onCommands;
private Command[] offCommands;
//保存上一次的命令对象
private Command undoCommand;
/*初始化remote对象时,为每个插槽对应的开关键装上空的命令对象*/
public Remote() {
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 setOneCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot]= offCommand;
}
/*指定插槽的开键被按下,调用对应命令对象的execute方法*/
public void onButtonPressed(int slot) {
System.out.println(slot + "号插槽的开键被按下");
onCommands[slot].execute();
undoCommand = onCommands[slot];
}
/*指定插槽的关键被按下,调用对应命令对象的execute方法*/
public void offButtonPressed(int slot) {
System.out.println(slot + "号插槽的关键被按下");
offCommands[slot].execute();
undoCommand = offCommands[slot];
}
public void undoButtonPressed() {
System.out.println("撤销了上次的操作");
undoCommand.undo();
}
}
测试:
1.8 使用状态实现撤销
打开关闭电灯,撤销上次对电灯的操作太过容易,通常为了实现撤销的功能,需要记录一些状态,比如说电灯,电灯有多种亮度,每种都是一种状态,电灯默认打开是中等亮度,我们调到高亮,按下撤销,希望电灯能回到中等亮度
我们重写电灯类:
/**
* @author 雫
* @date 2021/3/4 - 15:29
* @function 电灯
*/
public class Light {
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 int bright;
public Light() {
this.bright = OFF;
}
public int getBright() {
return bright;
}
public void on() {
System.out.println("打开电灯");
this.bright = MEDIUM;
}
public void high() {
System.out.println("最高亮度");
this.bright = HIGH;
}
public void low() {
System.out.println("最低亮度");
this.bright = LOW;
}
public void off() {
System.out.println("关闭电灯");
this.bright = OFF;
}
}
关于电灯的命令类:
/**
* @author 雫
* @date 2021/3/4 - 17:58
* @function 电灯低亮
*/
public class LightLowCommand implements Command {
private Light light;
private int preBright;
public LightLowCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
preBright = light.getBright();
light.low();
}
@Override
public void undo() {
if(preBright == Light.HIGH) {
light.high();
} else if(preBright == Light.MEDIUM) {
light.on();
} else if(preBright == Light.LOW) {
light.low();
} else if(preBright == Light.OFF) {
light.off();
}
}
}
/**
* @author 雫
* @date 2021/3/4 - 15:30
* @function 关灯命令
*/
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
this.light.off();
}
@Override
public void undo() {
this.light.on();
}
}
测试:
可以在Light中增加变量preBright记录上次的亮度,每次要执行关于电灯的命令时,通过get方法取到preBright,从而在undo中完成状态的撤销
1.9 宏命令
我们现在想要通过一个命令打开/关闭所有的家电,为此我们制作一对新的命令来完成这个需求:
/**
* @author 雫
* @date 2021/3/4 - 18:26
* @function 所有设备全部打开
*/
public class AllWorkCommand implements Command {
private Command[] commands;
public AllWorkCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
for(int i = 0; i < commands.length; i++) {
commands[i].execute();
}
}
@Override
public void undo() {
for(int i =0; i < commands.length; i++) {
commands[i].undo();
}
}
}
/**
* @author 雫
* @date 2021/3/4 - 18:28
* @function 所有设备全部关闭
*/
public class AllStopCommand implements Command {
private Command[] commands;
public AllStopCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
for(int i = 0; i < commands.length; i++) {
commands[i].execute();
}
}
@Override
public void undo() {
for(int i =0; i < commands.length; i++) {
commands[i].undo();
}
}
}
测试:
1.10 命令模式的更多用途
1,队列请求:
想象有一个工作队列,在一端添加命令,另一端则是线程,线程从队列中取出一个命令,然后调用它的excute()方法,等待这个调用完成后,取出下一个命令…
工作队列类和接收者是完全解耦的,工作队列中的线程不在乎接收者是谁,只会调用命令对象中的excute()方法,从而改变接收者的行为,此刻的线程可能在进行算数运算,下一刻就开始处理网络数据
2,日志请求:
某些应用需要我们将所有的动作都记录在日志中,并能在系统死机后,重新调用这些动作恢复到之前的状态,通过新增两个方法 store(),load(),命令模式能支持这一点
当我们执行命令时,每次执行完excute()时,同时执行store(),将本次操作存储在磁盘中,一旦系统死机,我们可以调用命令对象的load(),从磁盘中读取先前的数据,将程序恢复到死机前的状态
对于更高级的应用,这些技巧可以扩展到事物处理中,即一群操作必须全部进行完成,或者什么操作也没有进行
1.11 命令模式小结
命令模式允许我们将动作封装成命令对象,这样就可以随意存储,传递和调用它们
1,命令模式将发出请求的对象和执行请求的对象解耦
2,调用者通过调用命令对象的excute()方法发出请求,来控制接收者的行为
3,命令可以支持撤销,在每个命令对象中实现undo()方法来返回到excute()被执行前的状态
4,命令也可以用来实现日志和事物系统