程序员潇然 发表于 2022-8-2 15:16:51

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

命令模式(Command)

!(data/attachment/forum/202208/02/150842inaniuclu3at3wyu.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")


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

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

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


### 引子

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

```

```java
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中下发了三个命令

然后逐个执行这三个命令

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

```

```java
package command.origin;
public class Command {
    private BigDog bigDog;
    Command(BigDog bigDog) {
      this.bigDog = bigDog;
    }
    public void execute() {
      bigDog.goHomeForDinner();
    }
}
```

```java
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

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

### 结构

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

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


!(data/attachment/forum/202208/02/151103cqqz4cc1b4ukzd9b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")


**命令角色Command**

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

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

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

**请求者角色Invoker**

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

**接受者角色Receiver**

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

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

也就是一般形如:

Command(Receiver receiver){

......

execute(){

receiver.action();

...

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

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

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

### 结构代码示例

command角色

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



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

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


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

```java
package command;

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

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

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

客户端角色

```java
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();
    }
}
```

!(data/attachment/forum/202208/02/151309b87vnovvb9b1vb19.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")



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

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

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

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

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

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

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

### 代码示例

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

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

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

遥控器是请求者角色Invoker

用户是客户端角色Client

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

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

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

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

电视类Tv

```java
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接口

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


三个具体的命令类

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

```java
package command.tv;

public class OpenCommand implements Command {

    private Tv myTv;

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

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

```java
package command.tv;

public class CloseCommand implements Command {

    private Tv myTv;

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

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

```java
package command.tv;

public class ChangeChannelCommand implements Command {

    private Tv myTv;

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

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


遥控器Controller

拥有三个命令

```java
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

```java
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();
    }
}
```

!(data/attachment/forum/202208/02/151528j19ykswtewz91wwg.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")



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

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


### 命令请求记录

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

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

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

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

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

这就形成了一个队列

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

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

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

### 请求命令队列

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

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

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

```java
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角色,使之可以获得请求命令队列,并且执行命令请求队列的方法

```java
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方法

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

!(data/attachment/forum/202208/02/151629djepxsyhezr6sse7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")


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

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

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

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

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

!(data/attachment/forum/202206/16/141330jha7st9soow8772i.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "common_log.png")
`转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-117-1-1.html `
页: [1]
查看完整版本: 命令模式 Command 行为型 设计模式(十八)