命令模式 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]