目录
前言
最近看了《大话设计模式》中的命令模式。当看到命令模式的时候,感觉并不是太好理解,于是上网搜索了些资料。于是想写下自己对命令模式的一些看法,以加深理解。
本人也算是初学者,若有不对之处敬请指导更正,谢谢!
【总结:】非常建议大家手敲文中涉及到的代码,本文章将会从一个入门的项目开发,产生的问题,到如何使用命令模式去解决的说明。在第一次接触到命令模式时,我也是一脸懵逼,然后仿者文章手敲了两遍之后,才能初步意会到命令模式的精髓之处。
一、命令模式【what】
命令模式是一个高内聚的模式。它属于行为型模式,其定义为:将“请求”封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
其定义来说:是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
二、 命令模式场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队(如:线程池+工作队列)和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起,即支持宏命令。
前面1、3、4会在下面的案例详细说到。
三、 命令模式UML图
【理解:】
Client(客户):负责创建一个具体的命令(Concrete Command)
Invoker(调用者):调用者持有一个命令对象,并在某个时刻调用命令对象的 execute() 方法。
Command(命令接口):包含命令对象的 execute() 方法和 undo() 方法。
ConcreteCommand(具体命令):实现命令接口。包括两个操作,执行命令和撤销命令。
Receiver(接收者):接受命令并执行。
好比案例中:
智能遥控器(客户端)---> 遥控器上的各种按钮(调用命令着)---> 命令接口 ---> 具体命令 ----》 智能家居执行 (接收者)
个人认为,抛弃掉Client,直接有调用者--》命令接口---》具体命令---》接收者 四个更好理解一点
【这里有个小小套路】一般来说,如果调用的对象或者请求的操作是不确定的,那么为了解耦,习惯定义一个统一的接口,那么在调用方引入该接口类,然后具体的方法由子类去实现即可。
四、入门案例分析 ~ ~ (无使用命令模式)
上面的内容比较抽象,这里用一个案例加深对命令模式的理解!
需求:
- 家电自动化遥控器API项目
在这个遥控器上,有各种智能家电的API接口,去控制家电的使用情况 ~~ 两排按钮可认为是关闭与打开。
这里假设有:智能灯(多种类型,厨房灯、卧室灯)、音响两种智能家居。
传统的设计方案如下:
1、首先有灯类,该类中有灯关闭、灯打开的行为方法。
2、有音响类,该类中有音响关闭、音响打开、设置音量、得到音量的行为方法。
3、定义智能遥控器的接口类 ~~ 这一步可省略,但是我们是面向 接口的编程,所以应该写接口,然后实现类
4、定义智能遥控器的实现类 ~~ 其中在接口方法中实现了对智能家居行为的控制。
代码如下: ~~ 建议大家边敲代码,边往下看,不然真的不好理解,只有敲代码才会理解为什么这样做!
1、编写灯Light对象,该对象中有成员变量,表示灯名称,其中有两个成员方法 ~~ 灯关闭、灯打开
package com.dgut.edu.cn.commandmode.device;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/17 0017
* TIME: 下午 11:06
* Description:
*/
public class Light {
String loc = "";
public Light(String loc) {
this.loc = loc;
}
/**
* 电灯灯打开
*/
public void On(){
System.out.println(loc + "On");
}
public void Off(){
System.out.println(loc + "Off");
}
}
2、编写灯Stereo对象 ~~ 介绍详细看代码
package com.dgut.edu.cn.commandmode.device;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/17 0017
* TIME: 下午 11:15
* Description:
*/
public class Stereo {
// 定义当前音响的音量
static int volumn = 0;
/**
* 开启音响
*/
public void On(){
System.out.println("Stereo On");
}
/**
* 关闭音响
*/
public void Off(){
System.out.println("Stereo Off");
}
/**
* 设置音响音响
* @param vol
*/
public void SetVol(int vol){
volumn = vol;
System.out.println("Stereo volumn = " + volumn);
}
/**
* 为了判断是否达到了最大/最小音量的方法
* @return
*/
public int GetVol(){
return volumn;
}
/**
* 在命令模式会用到该方法,理解为放碟片,暂时不需要理解
*/
public void SetCd(){
System.out.println("Stereo set Cd");
}
}
3、编写遥控器对象的接口类 ~~ 打开接口、关闭接口
public interface Control {
public void onButton(int slot);
public void offButton(int slot);
}
4、编写遥控器对象的实现类,因为要控制灯、音响的具体行为,所以必须要有灯、音响的成员变量
package com.dgut.edu.cn.commandmode;
import com.dgut.edu.cn.commandmode.device.Light;
import com.dgut.edu.cn.commandmode.device.Stereo;
import sun.awt.geom.AreaOp;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/17 0017
* TIME: 下午 11:26
* Description:
*/
public class TradtionControl implements Control {
// 因为要控制灯、音响的具体行为,所以必须要有灯、音响的成员变量
public Light light;
public Stereo stereo;
public TradtionControl(Light light, Stereo stereo) {
this.light = light;
this.stereo = stereo;
}
/**
* 打开按钮的具体实现,根据传入的值去指定哪个电器打开
* @param slot
*/
@Override
public void onButton(int slot) {
switch (slot){
case 0:
light.On();
break;
case 1:
stereo.On();
break;
case 2:
int vol = stereo.GetVol();
if( vol < 11){
stereo.SetVol(++vol);
}
break;
}
}
/**
* 同理
* @param slot
*/
@Override
public void offButton(int slot) {
switch (slot){
case 0:
light.Off();
break;
case 1:
stereo.Off();
break;
case 2:
int vol = stereo.GetVol();
if( vol > 11){
stereo.SetVol(--vol);
}
break;
}
}
}
5、在测试方法中,去用指令打开灯,关闭灯
package com.dgut.edu.cn.commandmode;
import com.dgut.edu.cn.commandmode.device.Light;
import com.dgut.edu.cn.commandmode.device.Stereo;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/17 0017
* TIME: 下午 11:24
* Description:
*/
public class ControlTest{
public static void main(String[] args) {
Control ctl;
Light light = new Light("BedRoom");
Stereo stereo = new Stereo();
ctl = new TradtionControl(light,stereo);
ctl.onButton(0);
ctl.offButton(0);
ctl.onButton(1);
ctl.onButton(2);
ctl.offButton(2);
ctl.offButton(1);
}
}
结果演示:
BedRoomOn
BedRoomOff
Stereo On
Stereo volumn = 1
Stereo Off
项目源码截图 ~~ 后面会把源码分享,参考这五个文件
到此,我们就把当前的项目开发完毕!!
五、传统设计方案 有哪些不足?
耦合度高、扩展性低、维护性差。
体现在:调用者直接去使用接收者的行为方法,这样调用者会直接依赖于接收者。如果说现在想增加智能电视家居TV,那么必须在调用者中修改控制方法。如果说接收者修改了自己的行为方法,那么在调用者也要去修改对应的代码,这样的程序代码,维护性特别低,而且扩展性也很低,那么有没有一种方案很好的解决这个问题?
命令模式就可以帮助我们很好的解决这个方法
命令模式将 调用者、接收者分离开、两者通过命令接口、实现命令接口的具体类进行关联、在调用者中存在有setCommond()方法,手动的将命令传入到调用者,UML图如下 ~~ 如果不理解,可以参考一下上面关于命令模式的UML图啦 ~~
【理解:】
从上图就知道,
调用者、命令接口、命令、接收者、及出现这四个类型的原因。这样就很好的理解命令模式了把!!
至此,你应该了解命令模式的UML图,如果还是不理解,建议手敲一次代码。
六、使用命令模式改造该项目
1、接收者 ~~ 智能家居 灯 和 音响的功能实现类,跟上面1和2是一样的,代码参考上面
2、编写命令接口,定义统一接口说白了就是为了约束你的方法名是一样的,这样顶级父类调用子类的方法才会一样
package com.dgut.edu.cn.commandmode.command;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/17 0017
* TIME: 下午 11:50
* Description:
*/
public interface Command {
/**
* 定义所有指令的执行方法名为execute
*/
public void execute();
/**
* 定义相反的操作。关闭--开启,开启--关闭,减音响--加音量
*/
public void undo();
}
3、实现 灯关闭命令类 + 灯开启命令类
package com.dgut.edu.cn.commandmode.command;
import com.dgut.edu.cn.commandmode.device.Light;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/17 0017
* TIME: 下午 11:52
* Description:
*/
public class LightOnCommand implements Command {
public Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.On();
}
@Override
public void undo() {
light.Off();
}
}
package com.dgut.edu.cn.commandmode.command;
import com.dgut.edu.cn.commandmode.device.Light;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/17 0017
* TIME: 下午 11:52
* Description:
*/
public class LightOffCommand implements Command {
public Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.Off();
}
@Override
public void undo() {
light.On();
}
}
3、实现 音响关闭命令类 + 音响开启命令类 + 音响加音量命令类 + 音响减音量命令类
package com.dgut.edu.cn.commandmode.command;
import com.dgut.edu.cn.commandmode.device.Stereo;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/17 0017
* TIME: 下午 11:55
* Description:
*/
public class SteroOnCommand implements Command{
private Stereo stereo;
public SteroOnCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
stereo.On();
// 顺便选择CD,主要是为了掩饰同时执行两个操作
stereo.SetCd();
}
@Override
public void undo() {
stereo.Off();
}
}
package com.dgut.edu.cn.commandmode.command;
import com.dgut.edu.cn.commandmode.device.Stereo;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/17 0017
* TIME: 下午 11:59
* Description:
*/
public class SteroOffCommand implements Command {
private Stereo stereo;
public SteroOffCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
stereo.Off();
}
@Override
public void undo() {
stereo.On();
stereo.SetCd();
}
}
package com.dgut.edu.cn.commandmode.command;
import com.dgut.edu.cn.commandmode.device.Stereo;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/17 0017
* TIME: 下午 11:58
* Description:
*/
public class SteroAddVolCommand implements Command {
private Stereo stereo;
public SteroAddVolCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
int vol = stereo.GetVol();
if(vol < 11){
stereo.SetVol(++vol);
}
}
@Override
public void undo() {
int vol = stereo.GetVol();
if(vol > 0 ){
stereo.SetVol(--vol);
}
}
}
package com.dgut.edu.cn.commandmode.command;
import com.dgut.edu.cn.commandmode.device.Stereo;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/18 0018
* TIME: 上午 12:01
* Description:
*/
public class SteroSubVolCommand implements Command{
private Stereo stereo;
public SteroSubVolCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
int vol = stereo.GetVol();
if(vol > 0 ){
stereo.SetVol(--vol);
}
}
@Override
public void undo() {
int vol = stereo.GetVol();
if(vol < 11){
stereo.SetVol(++vol);
}
}
}
4、写一个空命令,主要是为了避免非空判断,在开发中经常遇到
package com.dgut.edu.cn.commandmode.command;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/18 0018
* TIME: 上午 12:02
* Description:
*/
public class NoCommand implements Command {
public NoCommand() {
}
@Override
public void execute() {
}
@Override
public void undo() {
}
}
5、命令类完成后,编写遥控器类
package com.dgut.edu.cn.commandmode.command;
import com.dgut.edu.cn.commandmode.Control;
import java.util.Stack;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/18 0018
* TIME: 上午 12:03
* Description:
*/
public class CommandModelControl implements Control {
private Command[] onCommands;
private Command[] offCommands;
private Stack<Command> stack = new Stack<Command>();
public CommandModelControl() {
onCommands = new Command[5];
offCommands = new Command[5];
Command noCommand = new NoCommand();
for(int i = 0 ; i < onCommands.length ; i++){
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
public void setCommand(int slot , Command onCommand,Command offCommand){
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
@Override
public void onButton(int slot) {
onCommands[slot].execute();
stack.add(onCommands[slot]);
}
@Override
public void offButton(int slot) {
offCommands[slot].execute();
stack.add( offCommands[slot]);
}
@Override
public void undoButton() {
stack.pop().undo();
}
}
6、编写测试类
package com.dgut.edu.cn.commandmode.command;
import com.dgut.edu.cn.commandmode.device.Light;
import com.dgut.edu.cn.commandmode.device.Stereo;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/18 0018
* TIME: 上午 12:10
* Description:
*/
public class ControlTest {
public static void main(String[] args) {
CommandModelControl control = new CommandModelControl();
Light bedroomlLight = new Light("bedRoom");
Light kitchLight = new Light("kitchLight");
Stereo stereo = new Stereo();
LightOnCommand bedroomLightOn = new LightOnCommand(bedroomlLight);
LightOffCommand bedrooLightOff = new LightOffCommand(bedroomlLight);
LightOnCommand kitchLightOn = new LightOnCommand(kitchLight);
LightOffCommand kitchLightOff = new LightOffCommand(kitchLight);
MarcoCommand onMarcoCommand = new MarcoCommand(new Command[]{bedroomLightOn,kitchLightOn});
MarcoCommand offMarcoCommand = new MarcoCommand(new Command[]{bedrooLightOff,kitchLightOff});
SteroOnCommand steroOnCommand = new SteroOnCommand(stereo);
SteroOffCommand steroOffCommand = new SteroOffCommand(stereo);
SteroAddVolCommand steroAddVolCommand = new SteroAddVolCommand(stereo);
SteroSubVolCommand steroSubVolCommand = new SteroSubVolCommand(stereo);
control.setCommand(0,bedroomLightOn,bedrooLightOff);
control.setCommand(1,kitchLightOn,kitchLightOff);
control.setCommand(2,steroOnCommand,steroOffCommand);
control.setCommand(3,steroAddVolCommand,steroSubVolCommand);
//control.setCommand(4,onMarcoCommand,offMarcoCommand);
control.onButton(0);
control.undoButton();
control.onButton(1);
control.undoButton();
control.onButton(2);
control.onButton(3);
control.offButton(3);
control.offButton(2);
/* control.onButton(4);
control.offButton(4);*/
}
}
7、运行结果
bedRoomOn
bedRoomOff
kitchLightOn
kitchLightOff
Stereo On
Stereo set Cd
Stereo volumn = 1
Stereo volumn = 0
Stereo Off
扩展1:支持撤销,回退的操作
需要记录每一步的操作,而且是先进先出,因此使用Stack,
那么可以在CommandModelControl类中看到:
private Stack<Command> stack = new Stack<Command>();
然后将每次的执行操作都存入到这个Stack中
public void setCommand(int slot , Command onCommand,Command offCommand){
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
@Override
public void onButton(int slot) {
onCommands[slot].execute();
stack.add(onCommands[slot]);
}
最后,在执行回退命令时,pop一个执行
@Override
public void undoButton() {
stack.pop().undo();
}
扩展2:支持宏命令 ~~ 也就是组合命令,一个命令可以同时操作多个智能家居
package com.dgut.edu.cn.commandmode.command;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/18 0018
* TIME: 上午 12:24
* Description:
*/
public class MarcoCommand implements Command{
private Command[] commands;
public MarcoCommand(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();
}
}
}
测试代码:
package com.dgut.edu.cn.commandmode.command;
import com.dgut.edu.cn.commandmode.device.Light;
import com.dgut.edu.cn.commandmode.device.Stereo;
/**
* create by IntelliJ IDEA
*
* @author : xiaozheng
* DATE: 2018/12/18 0018
* TIME: 上午 12:10
* Description:
*/
public class ControlTest {
public static void main(String[] args) {
CommandModelControl control = new CommandModelControl();
Light bedroomlLight = new Light("bedRoom");
Light kitchLight = new Light("kitchLight");
Stereo stereo = new Stereo();
LightOnCommand bedroomLightOn = new LightOnCommand(bedroomlLight);
LightOffCommand bedrooLightOff = new LightOffCommand(bedroomlLight);
LightOnCommand kitchLightOn = new LightOnCommand(kitchLight);
LightOffCommand kitchLightOff = new LightOffCommand(kitchLight);
MarcoCommand onMarcoCommand = new MarcoCommand(new Command[]{bedroomLightOn,kitchLightOn});
MarcoCommand offMarcoCommand = new MarcoCommand(new Command[]{bedrooLightOff,kitchLightOff});
SteroOnCommand steroOnCommand = new SteroOnCommand(stereo);
SteroOffCommand steroOffCommand = new SteroOffCommand(stereo);
SteroAddVolCommand steroAddVolCommand = new SteroAddVolCommand(stereo);
SteroSubVolCommand steroSubVolCommand = new SteroSubVolCommand(stereo);
control.setCommand(0,bedroomLightOn,bedrooLightOff);
control.setCommand(1,kitchLightOn,kitchLightOff);
control.setCommand(2,steroOnCommand,steroOffCommand);
control.setCommand(3,steroAddVolCommand,steroSubVolCommand);
control.setCommand(4,onMarcoCommand,offMarcoCommand);
/*control.onButton(0);
control.undoButton();
control.onButton(1);
control.undoButton();
control.onButton(2);
control.onButton(3);
control.offButton(3);
control.offButton(2);*/
control.onButton(4);
control.offButton(4);
}
}
演示结果: 同时打开双灯,同时关闭双灯
bedRoomOn
kitchLightOn
bedRoomOff
kitchLightOff
六、命令模式优点
- 类间解耦:调用者角色与接收者角色之间没有任何依赖关系,调用者实现功能时只需调用Command 抽象类的execute方法就可以,不需要了解到底是哪个接收者执行。
- 可扩展性:Command的子类可以非常容易地扩展,而调用者Invoker和高层次的模块Client不产生严 重的代码耦合。
- 命令模式结合其他模式会更优秀:命令模式可以结合责任链模式,实现命令族解析任务;结合模板方法模式,则可以减少 Command子类的膨胀问题。
七、命令模式缺点
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
请看Command的子类:如果有N个命令,问题就出来 了,Command的子类就可不是几个,而是N个,这个类膨胀得非常大,这个就需要读者在项 目中慎重考虑使用。
八、总结
通过对上面的分析我们可以知道如下几点:
1. 命令模式是通过命令发送者和命令执行者的解耦来完成对命令的具体控制的。
2. 命令模式是对功能方法的抽象,并不是对对象的抽象。
3. 命令模式是将功能提升到对象来操作,以便对多个功能进行一系列的处理以及封装。
好了,我对命令模式的理解到这里就结束了,如果大家发现有什么错误的地方,希望能不吝指正。如果有疑问的地方也可以提出,共同进步。
如果觉得文章对你有帮助,请点个赞喽,嘿嘿!