设计模式篇之——命令设计模式

Posted 风清扬逍遥子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式篇之——命令设计模式相关的知识,希望对你有一定的参考价值。

        命令设计模式很多人看了官方的文档是不够清晰的,甚至看了一遍基本记不住,说简单的谈不上,说难的话就那么一点代码,所以思想很重要,经过自己摸索后的一些理解,本文用最形象深刻的例子来带大家深刻理解命令设计模式的核心思路和最佳实践。

1、定义

        官方定义:命令模式是将请求封装成一个对象,从而让用户使用不同的请求把客户端参数化,以及支持可撤销和恢复的功能。

        咋一看不明白写的什么,什么请求封装对象,还有参数化,还有撤销和恢复?

        其实请求的意思是:客户端要求系统执行的操作,在Java里指的是某个对象的某个方法,如Animal的eat()方法,这个就是一个吃的请求。

        附一张图是结构介绍,看不明白的话看后面介绍的图对比着看:

2、组成部分

        命令模式有三部分组成

  • Command:请求封装成的对象,该对象是命令模式的主角。也就是说将请求方法封装成一个命令对象,通过操作命令对象来操作请求方法。在命令模式是有N个请求的,需要将这些请求封装成一条条命令对象,客户端只需要调用不同的命令就可以达到将请求参数化的目的。将一条条请求封装成一条条命令对象之后,客户端发起的就是一个个命令对象了,而不是原来的请求方法!
  • Receiver:有命令,自然会有命令的接收者对象了。如果有只有命令,没有接受者,那不就是光棍司令了?举个例子,如果没有电视机,你对着遥控器或者狂按有毛用?Receiver对象的主要作用就是受到命令后执行对应的操作。对于点击遥控器发起的命令来说,电视机就是这个Receiver对象,比如按了待机键,电视机收到命令后就执行了待机操作,进入待机状态。
  • Invoker:一条条命令已经组装成了一个一个Command,那么谁来使用或者调度这个命令呢?命令的使用者就是Invoker对象了。拿人,遥控器,电视机来做比喻,遥控器就是这个Invoker对象,遥控器负责使用客户端创建的命令对象。该Invoker对象负责要求命令对象执行请求,通常会持有命令对象,并且可以持有很多的命令对象。

        对于形象一点的图,我设计了一个看电视的场景:

 3、案例讲解

        如果我们不使用命令设计模式,来设计一个场景:

        我是一个屌丝,在家里看电视,我要看很多台比如CCTV1——CCTV6,而且我还可以返回上一个或者上N个台,我应该怎么实现?

        其中有四个个角色,我(Client),电视机(Receiver),遥控器(Invoker),还有我要下命令去按台(ConcreteCommand)

        先设计一个简单的电视机对象

/**
 * @Description:
 * @title: Television
 * @Author Star_Chen
 * @Date: 2021/12/20 23:21
 * @Version 1.0
 */
public class Television 

    public void playCCTV1() 
        System.out.println("--CCTV1--");
    

    public void playCCTV2() 
        System.out.println("--CCTV2--");
    

    public void playCCTV3() 
        System.out.println("--CCTV3--");
    

    public void playCCTV4() 
        System.out.println("--CCTV4--");
    

        电视机有了,写个屌丝Watcher,因为屌丝要看电视,所以要持有一个电视机的引用

/**
 * @Description:
 * @title: Watcher
 * @Author Star_Chen
 * @Date: 2021/12/20 23:22
 * @Version 1.0
 */
public class Watcher 

    /**
     * @Date: 2021/12/20 23:23
     * @Description: 持有Television
     */
    public Television tv;

    public Watcher(Television tv) 
        this.tv = tv;
    

    public void playCCTV1() 
        tv.playCCTV1();
    

    public void playCCTV2() 
        tv.playCCTV2();
    

    public void playCCTV3() 
        tv.playCCTV3();
    

    public void playCCTV4() 
        tv.playCCTV4();
    

        这个时候我们就可以看电视了,发个命令ClientMain

/**
 * @Description:
 * @title: ClientMain
 * @Author Star_Chen
 * @Date: 2021/12/20 23:23
 * @Version 1.0
 */
public class ClientMain 

    public static void main(String[] args) 
        Watcher watcher = new Watcher(new Television());
        watcher.playCCTV1();
        watcher.playCCTV2();
        watcher.playCCTV3();
        watcher.playCCTV4();
    

        执行结果很简单

--CCTV1--
--CCTV2--
--CCTV3--
--CCTV4--

        发现没有,如果我这个时候要添加一个CCTV5体育频道,要改动两个类,一个是Television,一个是屌丝Watcher,增加N个playCCTVN()方法;此时人和电视因为这些一个一个的指令被耦合一起了,等于屌丝要随时抱着电视。明显违背了“对修改关闭,对扩展开放“的重要设计原则。

        并且如果我想回退到刚刚的CCTV1,我是不是得这么写:

        为什么会这样呢?是因为对于之前播放的哪个卫视并没有记录功能。是时候让命令模式来出来解决问题了,通过命令模式的实现,对比下就能体会到命令模式的巧妙之处。 

        命令模式出场,分析下命令模式的几个关键Object,很明显这里的Television就是我们的Receiver,来接受命令的;那么谁来执行命令呢?很明显是遥控器RemoteControl;这个时候屌丝就不存在了,因为没有他的角色,你可以理解为他是Client;

        对于Command我们抽象出来一个一个接口

/**
 * @Description: 每个请求封装成的命令对象,客户端只需要调用不同的命令就可以达到将请求到不用的方法上
 * @title: Command
 * @Author Star_Chen
 * @Date: 2021/12/20 23:36
 * @Version 1.0
 */
public abstract class Command 

    /**
     * protected修饰的对象或者方法,子类
     * 1、父类的protected成员是在同一个包内可见的,并且对子类可见
     * 2、若子类与父类类不在同一包中,假如在两个不同的包里,那么在子类中,子类实例可以访问其从父类继承而来的protected方法,而不能访问父类实例(new出来的)的protected方法
     */
    protected Television television;

    /**
     * @Date: 2021/12/20 23:36
     * @Description: 抽象类的构造方法,是给子类使用的;
     */
    public Command(Television television) 
        this.television = television;
    

    /**
     * @Date: 2021/12/20 23:37
     * @Description: 抽象的命令执行方法
     */
    abstract void execute();

        有多少个CCTV台的指令,就实例化出多少个ConcreteCommand,这里我只写一个就可以了。

/**
 * @Description: CCTV1的执行命令
 * @title: CCTV1Command
 * @Author Star_Chen
 * @Date: 2021/12/20 23:19
 * @Version 1.0
 */
public class CCTV1Command extends Command 

    /**
     * @Date: 2021/12/20 23:20
     * @Description: 构造传入
     */
    public CCTV1Command(Television television) 
        super(television);
    

    /**
     * @Date: 2021/12/20 23:20
     * @Description:
     */
    @Override
    void execute() 
        television.playCctv1();
    

        接下来谁来接受命令呢,当然是Television作为Receiver

/**
 * @Description: 电视机类充当Receiver,命令的接受者对象
 * @title: Television
 * @Author Star_Chen
 * @Date: 2021/12/20 23:35
 * @Version 1.0
 */
public class Television 

    public void playCctv1() 
        System.out.println("--CCTV1--");
    

    public void playCctv2() 
        System.out.println("--CCTV2--");
    

    public void playCctv3() 
        System.out.println("--CCTV3--");
    

    public void playCctv4() 
        System.out.println("--CCTV4--");
    

    public void playCctv5() 
        System.out.println("--CCTV5--");
    

    public void playCctv6() 
        System.out.println("--CCTV6--");
    

        最后一个就是Invoker,谁来调用命令,当然是RemoteControl,因为不同的命令对象ConcreteCommand拥有共同的抽象类,我们很容易将这些名利功能放入一个数据结构(比如数组)中来存储执行过的命令;遥控器对象持有一个命令集合,用来记录已经执行过的命令。新的命令对象作为switchTV参数来添加到集合中,注意在这里就体现出了让上文所说的请求参数化的目的,来达成撤回或者恢复效果

/**
 * @Description: 遥控器,充当invoker
 * @title: RemoteControl
 * @Author Star_Chen
 * @Date: 2021/12/20 23:35
 * @Version 1.0
 */
public class RemoteControl 

    List<Command> historyCommand = new ArrayList<Command>();

    /**
     * @Date: 2021/12/20 23:40
     * @Description: 切换卫视
     */
    public void switchTV(Command command) 
        historyCommand.add(command);
        command.execute();
    

    /**
     * @Date: 2021/12/20 23:39
     * @Description: 遥控器返回命令
     */
    public void back() 
        if (historyCommand.isEmpty()) 
            return;
        
        int size = historyCommand.size();
        int preIndex = size - 2 <= 0 ? 0 : size - 2;

        //获取上一个播放某卫视的命令
        Command preCommand = historyCommand.remove(preIndex);

        preCommand.execute();
    

        那么剩下就是Client,也就是Main

/**
 * @Description: 充当Client
 * @title: CommandMain
 * @Author Star_Chen
 * @Date: 2021/12/20 23:43
 * @Version 1.0
 */
public class CommandMain 

    public static void main(String[] args) 
        Television tv = new Television();

        RemoteControl remoteControl = new RemoteControl();
        remoteControl.switchTV(new CCTV1Command(tv));
        remoteControl.switchTV(new CCTV2Command(tv));
        remoteControl.switchTV(new CCTV3Command(tv));    
        //切换上一个台
        remoteControl.back();
    

4、总结

        从上面的例子我们可以看出,命令模式的主要特点就是将请求封装成一个个命令,以命令为参数来进行切换,达到请求参数化的目的,且还能通过集合这个数据结构来存储已经执行的请求,进行回退操作。而且如果需要添加新的电视频道,只需要添加新的命令类即可。

        那么有没有缺点呢?当然有,如果这些请求越多,意味着要执行的命令越多,项目里会大量的存在Command的实现类,虽然满足了单一职责,但是也太单一了,维护是个很麻烦的事情;

        优点也看得出来,在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者(屌丝)"与"行为实现者(电视)"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合,这是命令模式的使用场景,说白了就是借助个中间层面的东西——命令,来实现解耦。

        好了,我对命令模式的理解到这里就结束了,如果大家发现有什么错误的地方,希望能不吝指正!!

以上是关于设计模式篇之——命令设计模式的主要内容,如果未能解决你的问题,请参考以下文章

设计模式篇之——命令设计模式

Linux命令篇之stat命令和cp命令

一篇文章学会shell工具篇之sed

设计模式篇之——策略设计模式

java基础第十七篇之网络编程和装饰者模式

设计模式篇之——策略设计模式