请选择 进入手机版 | 继续访问电脑版

[设计模式] 命令模式 Command 行为型 设计模式(十八)

计算机科学 计算机科学 1367 人阅读 | 0 人回复

命令模式(Command)

image.png

请分析上图中这条命令的涉及到的角色以及执行过程,一种可能的理解方式是这样子的:

涉及角色为:大狗子和大狗子他妈

过程为:大狗子他妈角色 调用 大狗子的“回家吃饭”方法

引子

package command.origin;
public class BigDog {
    public void goHomeForDinner() {
    System.out.println("回家吃饭");
  }
}
package command.origin;

public class BigDogMother {
public static void main(String[] args) {
  BigDog bigDog = new BigDog();
    bigDog.goHomeForDinner();
  }
}

BigDog类拥有回家吃饭方法goHomeForDinner

BigDogMother作为客户端调用BigDog的回家吃饭方法,完成了“大狗子回家吃饭”这个请求

上面的示例中,通过对命令执行者的方法调用,完成了命令的下发,**命令调用者与命令执行者之间是紧密耦合的**

我们是否可以考虑换一种思维方式,将“你妈喊你回家吃饭”这一命令封装成为一个对象?

不再是大狗子他妈调用大狗子的回家吃饭方法

而是大狗子他妈下发了一个命令,命令的内容是“大狗子回家吃饭”

接下来是命令的执行

这样的话,“命令”就不再是一种方法调用了,在大狗子妈和大狗子之间多了一个环节---“命令”

看下代码演变

BigDog 没有变化

新增加了命令类Command 使用对象的接受者BigDog 进行初始化

命令的execute方法内部调用接受者BigDog的方法

BigDogMother中下发了三个命令

然后逐个执行这三个命令

package command.origin;
public class BigDog {
public void goHomeForDinner() {
    System.out.println("回家吃饭");
  }
}
package command.origin;
public class Command {
    private BigDog bigDog;
    Command(BigDog bigDog) {
        this.bigDog = bigDog;
    }
    public void execute() {
        bigDog.goHomeForDinner();
    }
}
package command.origin;
public class BigDogMother {
    public static void main(String[] args) {
        BigDog bigDog = new BigDog();
        Command command1 = new Command(bigDog);
        Command command2 = new Command(bigDog);
        Command command3 = new Command(bigDog);

        command1.execute();
        command2.execute();
        command3.execute();
    }
}

从上面的代码示例中看到,通过对“请求”也就是“方法调用”的封装,将请求转变成了一个个的命令对象

命令对象本身内部封装了一个命令的执行者

好处是:命令可以进行保存传递了,命令发出者与命令执行者之间完成了解耦,命令发出者甚至不知道具体的执行者到底是谁

而且执行的过程也更加清晰了

意图

将一个请求封装为一个对象,从而使可用不同的请求对客户进行参数化;

对请求排队或者记录请求日志,以及支持可撤销的操作。

别名 行为Action或者事物Transaction

命令模式就是将方法调用这种命令行为或者说请求 进一步的抽象,封装为一个对象

结构

上面的“大狗子你妈喊你回家吃饭”的例子只是展示了对于“命令”的一个封装。只是命令模式的一部分。

下面看下命令模式完整的结构

image.png

命令角色Command

声明了一个给所有具体命令类的抽象接口

做为抽象角色,通常是接口或者实现类

具体命令角色ConcreteCommand 定义一个接受者和行为之间的弱耦合关系,实现execute()方法 负责调用命令接受者的响相应操作

请求者角色Invoker

负责调用命令对象执行命令,相关的方法叫做行动action方法

接受者角色Receiver

负责具体实施和执行一个请求,任何一个类都可以成为接收者

Command角色封装了命令接收者并且内部的执行方法调用命令接收者的方法

也就是一般形如:

Command(Receiver receiver){

......

execute(){

receiver.action();

...

而Invoker角色接收Command,调用Command的execute方法

通过将“命令”这一行为抽象封装,命令的执行不再是请求者调用被请求者的方法这种强关联 ,而是可以进行分离

分离后,这一命令就可以像普通的对象一样进行参数传递等

结构代码示例

command角色

package command;
public interface Command {
    void execute();
}

ConcreateCommand角色 内部拥有命令接收者,内部拥有execute方法

package command;
public class ConcreateCommand implements Command {
    private Receiver receiver;
    ConcreateCommand(Receiver receiver) {
        this.receiver = receiver;
    }
    @Override
    public void execute() {
        receiver.action();
    }
}

Receiver命令接收者,实际执行命令的角色

package command;

public class Receiver {
    public void action(){
        System.out.println("command receiver do sth....");
    }
}

命令请求角色Invoker 用于处理命令,调用命令角色执行命令

package command;
public class Invoker {
    private Command command;
    Invoker(Command command){
        this.command = command;
    }
    void action(){
        command.execute();
    }
}

客户端角色

package command;
public class Client {
    public static void main(String[] args){
        Receiver receiver = new Receiver();
        Command command = new ConcreateCommand(receiver);
        Invoker invoker = new Invoker(command);
        invoker.action();
    }
}

image.png

在客户端角色的测试代码中,我们创建了一个命令,指定了接收者(实际执行者)

然后将命令传递给命令请求调用者

虽然最终命令的接收者为receiver,但是很明显如果这个Command是作为参数传递进来的

Client照样能够运行,他只需要借助于Invoker执行命令即可

命令模式关键在于:引入命令类对方法调用这一行为进行封装

命令类使的命令发送者与接收者解耦,命令请求者通过命令类来执行命令接收者的方法

而不在是直接请求命名接收者

代码示例

假设电视机只有三个操作:开机open 关机close和换台change channel。

用户通过遥控器对电视机进行操作。

电视机本身是命令接收者 Receiver

遥控器是请求者角色Invoker

用户是客户端角色Client

需要将用户通过遥控器下发命令的行为抽象为命令类Command

Command有开机命令 关机命令和换台命令

命令的执行需要借助于命令接收者

Invoker 调用Command的开机命令 关机命令和换台命令

电视类 Tv

package command.tv;

public class Tv {
    public void turnOn(){
        System.out.println("打开电视");
    }

    public void turnOff(){
        System.out.println("关闭电视");
    }
    public void changeChannel(){
        System.out.println("换台了");
    }
}

Command接口

package command.tv;
public interface Command {
    void execute();
}

三个具体的命令类

内部都保留着执行者,execute方法调用他们的对应方法

package command.tv;

public class OpenCommand implements Command {

    private Tv myTv;

    OpenCommand(Tv myTv) {
        this.myTv = myTv;
    }

    @Override
    public void execute() {
        myTv.turnOn();
    }
}
package command.tv;

public class CloseCommand implements Command {

    private Tv myTv;

    CloseCommand(Tv myTv) {
        this.myTv = myTv;
    }

    @Override
    public void execute() {
        myTv.turnOff();
    }
}
package command.tv;

public class ChangeChannelCommand implements Command {

    private Tv myTv;

    ChangeChannelCommand(Tv myTv) {
        this.myTv = myTv;
    }

    @Override
    public void execute() {
        myTv.changeChannel();
    }
}

遥控器Controller

拥有三个命令

package command.tv;
public class Controller {
    private Command openCommand = null;
    private Command closeCommand = null;
    private Command changeChannelCommand = null;

    public Controller(Command on, Command off, Command change) {
        openCommand = on;
        closeCommand = off;
        changeChannelCommand = change;
    }

    public void turnOn() {
        openCommand.execute();
    }

    public void turnOff() {
        closeCommand.execute();
    }

    public void changeChannel() {
        changeChannelCommand.execute();
    }
}

用户类User

package command.tv;
public class User {
    public static void main(String[] args) {
        Tv myTv = new Tv();
        OpenCommand openCommand = new OpenCommand(myTv);
        CloseCommand closeCommand = new CloseCommand(myTv);
        ChangeChannelCommand changeChannelCommand = new ChangeChannelCommand(myTv);
        Controller controller = new Controller(openCommand, closeCommand, changeChannelCommand);
        controller.turnOn();
        controller.turnOff();
        controller.changeChannel();
    }
}

image.png

以上示例将电视机的三种功能开机、关机、换台 抽象为三种命令

一个遥控器在初始化之后,就可以拥有开机、关机、换台的功能,但是却完全不知道底层的实际工作的电视。

命令请求记录

一旦将“发起请求”这一行为进行抽象封装为命令对象

那么“命令”也就具有了一般对象的基本特性,比如,作为参数传递

比如使用容器存放进行存放

比如定义一个ArrayList 用于保存命令

ArrayList<Command> commands = new ArrayList<Command>();

这就形成了一个队列

你可以动态的向队列中增加命令,也可以从队列中移除命令

你还可以将这个队列保存起来,批处理的执行或者定时每天的去执行

你还可以将这些命令请求持久化到文件中,因为这些命令、请求 也不过就是一个个的对象而已

请求命令队列

既然可以使用容器存放命令对象,我们可以实现一个命令队列,对命令进行批处理

新增加一个CommandQueue类,内部使用ArrayList存储命令

execute()方法,将内部的请求命令队列全部执行

package command;
import java.util.ArrayList;

public class CommandQueue {

    private ArrayList<Command> commands = new ArrayList<Command>();

    public void addCommand(Command command) {
        commands.add(command);
    }

    public void removeCommand(Command command) {
        commands.remove(command);
    }

    //执行队列内所有命令
    public void execute() {
        for (Object command : commands) {
            ((Command) command).execute();
        }
    }
}

同时调整Invoker角色,使之可以获得请求命令队列,并且执行命令请求队列的方法

package command;
public class Invoker {
    private Command command;
    Invoker(Command command) {
        this.command = command;
    }
    void action() {
        command.execute();
    }
    //新增加命令队列
    private CommandQueue commandQueue;
    public Invoker(CommandQueue commandQueue) {
        this.commandQueue = commandQueue;
    }
    /*
     * 新增加队列批处理方法*/
    public void batchAction() {
        commandQueue.execute();
    }
}

从上面的示意代码可以看得出来,请求队列的关键就是命令类

一旦创建了命令类,就解除了命令请求者与命令接收者之间耦合,就可以把命令当做一个普通对象进行处理,调用他们的execute()执行方法

所谓请求队列不就是使用容器把命令对象保存起来,然后调用他们的execute方法嘛

所以说,命令请求的对象化,可以实现对请求排队或者记录请求日志的目的,就是命令对象的队列

宏命令

计算机科学里的宏(Macro),是一种批量批处理的称谓

一旦请求命令"对象化",就可以进行保存

上面的请求队列就是如此,保存起来就可以实现批处理的功能,这就是命令模式的宏命令

撤销操作

在上面的例子中,我们没有涉及到撤销操作

命令模式如何完成“撤销”这一行为呢?

命令是对于请求这一行为的封装抽象,每种ConcreteCommand都对应者接收者一种具体的行为方式

所以想要能够有撤销的行为,命令接收者(最终的执行者)必然需要有这样一个功能

如果Receiver提供了一个rollback方法

也就是说如果一个receiver有两个方法,action()和rollback()

当执行action方法后,调用rollback可以将操作进行回滚

那么,我们就可以给Command增加一个方法,recover() 用于调用receiver 的rollback方法

这样一个命令对象就有了两种行为,执行execute和恢复recover

如果我们在每次的命令执行后,将所有的 执行过的 命令保存起来

当需要回滚时,只需要逐个(或者按照执行的相反顺序)执行命令对象的recover方法即可

这就很自然的完成了命令的撤销行为,而且还可以批量进行撤销

命令模式的撤销操作依赖于命令接收者本身的撤销行为,如果命令接收者本身不具备此类方法显然没办法撤销

另外就是依赖对执行过的命令的记录

使用场景

对于“大狗子你妈喊你回家吃饭”的例子,我想你也会觉得大狗子妈直接调用大狗子的方法就好了

脱裤子放屁,抽象出来一个命令对象有什么用呢?

对于简单的方法调用,个人也认为是自找麻烦

命令模式是有其使用场景以及特点的,并不是说不分青红皂白的将请求处理都转换为命令对象

到底什么情况需要使用命令模式?

通过上面的分析,如果你希望将请求进行排队处理,或者请求日志的记录

那么你就很可能需要命令模式,只有将请求转换为命令对象,这些行为才更易于实现

如果系统希望支持撤销操作

通过请求的对象化可以方便的将命令的执行过程记录下来,就下来之后,就形成了“操作记录”

拥有了操作记录,如果有撤销方法,就能够执行回滚撤销

如果希望命令能够被保存起来组成宏命令,重复执行或者定时执行等,就可以使用命令模式

如果希望将请求的调用者和请求的执行者进行解耦,使得请求的调用者和执行者并不直接接触

命令对象封装了命令的接收者,请求者只关注命令对象,根本不知道命令的接收者

如果希望请求具有更长的生命周期,普通方法调用,命令发出者和命令执行者具有同样的生命周期

命令模式下,命令对象封装了请求,完成了命令发出者与命令接收者的解耦

命令对象创建后,只依赖命令接收者的执行,只要命令接收者存在,就仍旧可以执行,但是命令发出者可以消亡

总之命令模式的特点以及解决的问题,也正是他适用的场景

这一点在其他模式上也一样

特点以及解决的问题,也正是他适用的场景,适用场景也正是它能解决的问题

总结

命令模式中对于场景中命令的提取,始终要注意它的核心“对接收者行为的命令抽象

比如,电视作为命令接收者,开机,关机,换台是他自身固有的方法属性,你的命令也就只能是与之对应的开机、关机、换台

你不能打游戏,即使你能打游戏,电视也不会让你打游戏

这是具体的命令对象ConcreteCommand的设计思路

Command提供抽象的execute方法,所有的命令都是这个方法

调用者只需要执行Command的execute方法即可,不关注到底是什么命令,命令接收者是谁

如果命令的接收者有撤销的功能,命令对象就可以也同样支持撤销操作

关于如何抽取命令只需要记住:

命令模式中的命令对象是请求的封装,请求基本就是方法调用,方法调用就是需要方法的执行者,也就是命令的接收者有对应行为的方法

请求者和接收者通过命令对象进行解耦,降低了系统的耦合度

命令的请求者Invoker与命令的接收者Receiver通过中间的Command进行连接,Command中的协议都是execute方法

所以,如果新增加命令,命令的请求者Invoker完全不需要做任何更改,他仍旧是接收一个Command,然后调用他的execute方法

具有良好的扩展性,满足开闭原则

image.png

回到刚才说的,具体的命令对象ConcreteCommand的设计思路

需要与命令接收者的行为进行对应

也就是针对每一个对请求接收者的调用操作,都需要设计一个具体命令类,可能会出现大量的命令类

有一句话说得好,“杀鸡焉用宰牛刀”,所以使用命令模式一定要注意场景

以免被别人说脱裤子放屁,为了用设计模式而用设计模式....

common_log.png 转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-117-1-1.html

关注下面的标签,发现更多相似文章

文章被以下专栏收录:

    黄小斜学Java

    疯狂的字节X

  • 目前专注于分享Java领域干货,公众号同步更新。原创以及收集整理,把最好的留下。
    包括但不限于JVM、计算机科学、算法、数据库、分布式、Spring全家桶、微服务、高并发、Docker容器、ELK、大数据等相关知识,一起进步,一起成长。
热门推荐
[CXX1300] CMake '3.18.1' was not
[md][CXX1300] CMake '3.18.1' was not found in SDK, PATH, or
[若依]微服务springcloud版新建增添加一个
[md]若依框架是一个比较出名的后台管理系统,有多个不同版本。
解决waiting for all target devices to co
[md]解决Launching app ,waiting for all target devices to co