设计模式大总结(六):命令模式

前言

最近看了命令模式,觉得很有意思,平时大家应该都有使用过,所以写一点总结体会分享给大家。

正文

首先我们先不谈什么是命令模式,直接写点东西:

实现一个电视遥控器的功能:
1、遥控器有两个键:开机键和关机键。
2、电视接收对应的命令信号,执行对应的操作。

ok,首先我们知道命令是一个抽象的概念,所以我们先写一个Command借口:

/**
 * Created by li.zhipeng on 2017/9/26.
 *      命令接口
 */
public interface Command {

    void onCommand();
}

因为遥控器要绑定对应的电视机,所以我们的遥控器按钮构造方法里或者是setter能够设置绑定的电视机,这样我们创建一个基类,我们的目的是为了在基类复杂一些基本信息,例如按钮的位置,颜色等公有特性:

/**
 *      遥控器按钮的基类
 */
public abstract class ControllerButton {

    private TV tv;

    public ControllerButton(TV tv){
        this.tv = tv;
    }

}

下面定义开机键和关机键的功能:

/**
 * 开机功能
 */
public class OpenCommand implements Command {
    @Override
    public void onCommand() {
        System.out.println("打开电视...");
        tv.open();
    }
}

/**
 * 关机功能
 */
public class CloseCommand implements Command {
    @Override
    public void onCommand() {
        System.out.println("关闭电视...");
        tv.close();
    }
}

使用时的代码:

TV tv = new TV();

Command openCommand = new OpenCommand(tv);
Command closeCommand = new CloseCommand(tv);

openCommand.onCommand();
closeCommand.onCommand();

功能的扩展

刚才我们已经完成了基本功能的开发,我们的类的定义还是比较严格的,基类和接口都只做自己相关的事情,我们定义的每一个类都尽可能的少承担起整个功能的责任,符合我们开发的基本原则。

例如,我们可以去掉Command接口,把onCommand移动到基类ControllerButton中,但是这样就加重了基类的负担,违背了我们开发的基本准则,也影响到了之后的功能更扩展,所以不建议这样做的。

但是很快新需求来了:

在发送命令的时候,需要同时发送一条指示灯闪闪闪的命令,这样用户知道自己的命令到底发没发出去,有助于用户体验。

现在只有两个按钮,我们只要在调用的时候,添加上指示灯闪闪的命令就可以了:

// 红灯闪闪命令
lampBulingCommand.onCommand();
openCommand.onCommand();

这个时候一定有人站出来了:

我现在就要把Command去掉,直接在基类ControllerButton里面实现onCommand方法,这样不用修改调用方法,比你这种实现吊太多!!!

不得不说这是一个好办法,以现在这个需求来看修改基类是最简单的,但是违背了我们之前的开发原则,但是为了这位朋友的任性,我们暂时不阻止他,于是基类的代码发生了改变:

/**
 *      遥控器按钮的基类
 */
public abstract class ControllerButton implements Command{

    protected TV tv;

    /**
     * 指示灯闪闪命令
     * */
    private LampBulingCommand lampBulingCommand;

    public ControllerButton(TV tv){
        this.tv = tv;
        this.lampBulingCommand = lampBulingCommand;
    }
    
    /**
     * 对onCommand进行包装
     * */
     public void sendCommand(){
        if (lampBulingCommand != null){
            lampBulingCommand.onCommand();
        }
        onCommand();
    }

}

// 调用处,请注意这里已经无法Command,因为sendCommand定义在基类里,而不是在Command接口里
OpenCommand openCommand = new OpenCommand(tv);
openCommand.sendCommand();

我们看到了,为了实现产品的需求,他做了3处修改:

1、添加属性,保存指示灯闪闪命令。
2、修改构造方法,实例化指示灯闪闪命令。
3、在onCommand外部定义了一个中转函数,执行指示灯闪闪命令。
4、修改调用的代码。
5、最关键:已经无法通过Command类型创建方法。

但是他仍然陶醉在自己的世界里,认为这个方法屌爆了。

但是很遗憾,很快新需求又来了:

在新增一条记忆命令,我们需要统计开机的次数,但是不统计关机命令的次数。

虽然这个需求有点变态了,但是我们还得硬着头皮继续写,这个时候刚才的那位朋友又站出来了,分分钟就要解决这个问题:

/**
 *      遥控器按钮的基类
 */
public abstract class ControllerButton implements Command{

    protected TV tv;

    /**
     * 指示灯闪闪命令
     * */
    private LampBulingCommand lampBulingCommand;
    
    /**
    * 记忆命令
    */
    private MemoryCommand memoryCommand;

    public ControllerButton(TV tv){
        this.tv = tv;
        this.lampBulingCommand = lampBulingCommand;
        memoryCommand = new MemoryCommand(tv);
    }
    
    /**
     * 对onCommand进行包装
     * */
     public void sendCommand(){
        // 指示灯闪闪
        lampBulingCommand.onCommand();
        // 如果是开机功能,要发送记忆命令
        if (this instanceof OpenCommand) {
            memoryCommand.onCommand();
        }
        onCommand();
    }

}

经过修改后的代码,虽然运行正常,他自己已经感觉到自己挖的坑越来越深,而其他人也出现了怀疑的态度,因为:

1、父类已经开始影响到子类的业务逻辑。

2、基类越来越臃肿:每一次内部的属性的增加和sendCommand方法的复杂度的上升,都让基类的变得越来越臃肿,并且基类已经开始越权处理onCommand的逻辑,Command接口已经形同虚设,类的阅读和维护都开始出现了问题。

3、已经无法通过Command创建命令实例,全部要被替换成ControllerButton,类的语义出现了严重的危机。

命令模式登场

经过之前的讨论,命令模式终于登场了,于是咔咔咔重构了代码:

/**
 * 命令的执行者,处于命令发起者和接收者之间,在这个过程中进行处理
 */
public class Switch {

    private LampBulingCommand lampBulingCommand;

    private MemoryCommand memoryCommand;

    public Switch(TV tv) {
        lampBulingCommand = new LampBulingCommand(tv);
        memoryCommand = new MemoryCommand(tv);
    }

    public void excuteCommand(Command command) {
        // 指示灯闪闪
        lampBulingCommand.onCommand();
        // 如果是开机功能,要发送记忆命令
        if (command instanceof OpenCommand) {
            memoryCommand.onCommand();
        }
        // 执行参数命令
        command.onCommand();
    }

}

修改调用的代码:

TV tv = new TV();
Command openCommand = new OpenCommand(tv);
Command closeCommand = new CloseCommand(tv);
//openCommand.onCommand();
//closeCommand.onCommand();
Switch s = new Switch(tv);
s.excuteCommand(openCommand);
s.excuteCommand(closeCommand);

是不是完全被惊艳到了,就是简单~

现在开始进入正题:什么是命令模式

命令模式是把一个操作和他的参数,包装成一个新的对象去执行。
命令模式有四个部分:命令,接收者,执行者,发起者。

以刚才的demo为例,Command代表命令,接收者是TV,执行者是Switch,发起者也就是客户端。

从demo中看到,随着需求的变化,我们的每一次修改都要修改多个类,并且代码的成本也很高,于是通过Switch把Command的执行过程包装了起来,也就是在发起者和接收者之间,随意我们就可以根据需求,定制执行的过程。

也可以理解成,Switch把基类中有关于Command的功能全部抽取了出来,作为一个独立模块。

经过命令模式的重构,我们之后的扩展和修改,只要不改变open和close的核心功能,只要修改Switch类就可以了,这就是命令模式的优点。

总结

最后我们对命令模式进行一下总结:

1、命令模式是对某一个操作的和其参数的封装,目的是维护这个操作的过程。

2、命令模式位于发起者和接收者之间,对两者进行解耦,便于维护。

3、命令模式能够帮助我们明确类和接口的定义的目的,理解面向对象编程。

顺便强调一下,过多的if-else是糟糕的代码,凸显出程序的笨重,例如demo中,我们可以利用开关来解决:

/**
*  在基类中增加boolean型开关,并增加参数为ControllerButton的方法
*/
public class Switch {

    ...

    public void excuteCommand(ControllerButton controllerButton) {
        // 如果是开机功能,要发送记忆命令
        if (controllerButton.isNeedMemory()) {
            memoryCommand.onCommand();
        }
        excuteCommand(controllerButton);
    }

    public void excuteCommand(Command command) {
        // 指示灯闪闪
        lampBulingCommand.onCommand();
        // 执行参数命令
        command.onCommand();
    }

}

最后要说的是,不要随意违背开发的基本原则,例如上面的那位朋友,这些规则是前辈经过长时间的研究总结的结晶,当然这些经验不一定是对的,也不适于所有的场景,但是如果你非要这么做,请做好充足的准备。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 1 场景问题# 1.1 如何开机## 估计有些朋友看到这个标题会非常奇怪,电脑装配好了,如何开机?不就是按下启动按...
    七寸知架构阅读 2,800评论 1 59
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,448评论 25 707
  • 1.下载cuda sdk,并安装 https://developer.nvidia.com/cuda-downlo...
    G风阅读 1,182评论 0 0
  • “终身所约,永结为好,琴瑟再御,岁月静好” “即使身败名裂,受万人责辱,我也愿为你冒这个天下大不为……” 幼时同你...
    悯欣儿阅读 225评论 0 0