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

作者: 珠穆朗玛小王子 | 来源:发表于2017-09-26 18:10 被阅读0次

    前言

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

    正文

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

    实现一个电视遥控器的功能:
    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();
        }
    
    }
    

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

    相关文章

      网友评论

        本文标题:设计模式大总结(六):命令模式

        本文链接:https://www.haomeiwen.com/subject/fkheextx.html