1.简介
命令模式是一种行为设计模式,他可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行,或将其放入到队列中,且能实现可撤销操作。
实现方式:
-
声明仅有一个执行方法的命令接口。
-
抽取请求并使之成为实现命令接口的具体命令类。每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。所有这些变量的数值都必须通过命令构造函数进行初始化。
-
找到担任发送者职责的类,在这些类中添加保存命令的成员变量。发送者只能通过命令接口与其命令进行交互。发送者自身通常不创建命令对象,而是通过客户端代码获取。
-
修改发送者使其执行命令,而非直接将请求发送给接收者。
-
客户端必须按照以下顺序来初始化对象:
1.创建接收者
2.创建命令,如有需要可将其关联至接收者
3.创建发送者并将其与特定命令关联
2.UML图
1.发送者 (Sender)——亦称 “触发者 (Invoker)”——类负责对请求进行初始化, 其中必须包含一个成员变量来存储对于命令对象的引用。 发送者触发命令, 而不向接收者直接发送请求。 注意, 发送者并不负责创建命令对象: 它通常会通过构造函数从客户端处获得预先生成的命令。
2.命令 (Command) 接口通常仅声明一个执行命令的方法。
3.具体命令 (Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务逻辑对象。 但为了简化代码, 这些类可以进行合并。
接收对象执行方法所需的参数可以声明为具体命令的成员变量。 你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化。
4.接收者 (Receiver) 类包含部分业务逻辑。 几乎任何对象都可以作为接收者。 绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作。
5.客户端 (Client) 会创建并配置具体命令对象。 客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。 此后, 生成的命令就可以与一个或多个发送者相关联了。
3.代码实现
eg:假设现在有这么一个场景:
餐厅中
- 客户需要下订单给服务员
- 服务员拿到订单后,统一放在订单柜台,告诉厨师来订单了
- 厨师依据订单来做具体的食物
以前我们的传统实现方式一般是服务员对象直接聚合厨师对象,调用厨师根据订单做菜的方法,这种实现就出现服务员对象和厨师对象的强耦合,如果以后换厨师就会需要修改代码,不符合开闭原则。而,命令模式可以很好的解决该问题。
订单类:
其实就是接收者所需的请求参数,这里用一个实体来包含。
package com.gs.designmodel.ordermodel.orderfood;
import java.util.HashMap;
import java.util.Map;
/**
* @author: Gaos
* @Date: 2023-01-16 11:50
*
*
* 订单类
**/
public class Order {
// 餐桌号码
private int diningTable;
// 餐桌下 菜品名称及份数
private Map<String, Integer> foodDir = new HashMap<>();
public int getDiningTable() {
return diningTable;
}
public void setDiningTable(int diningTable) {
this.diningTable = diningTable;
}
public Map<String, Integer> getFoodDir() {
return foodDir;
}
public void setFood(String name,int num) {
foodDir.put(name,num);
}
}
厨师类:
是命令的Receiver(接收者角色)
package com.gs.designmodel.ordermodel.orderfood;
/**
* @author: Gaos
* @Date: 2023-01-16 11:54
*
* 厨师类,是命令的执行者
**/
public class SeniorChef {
// 根据餐品及份数制作食物
public void makeFood(String name, int num) {
System.out.println(num + "份" + name);
}
}
抽象命令类:
package com.gs.designmodel.ordermodel.orderfood;
/**
* @author: Gaos
* @Date: 2023-01-16 13:55
*
* 抽象命令类
*
**/
public interface Command {
/**
* 命令执行方法
*/
void execute();
/**
* 命令记录
*/
void undo();
}
订单命令类:
属于具体的命令类, 需要聚合 对象接收者和接收者依赖的操作数据.
所以其中有两个成员变量,一个是接受者对象(需要依靠它来执行具体的execute方法),另一个是接受者依赖的操作数据,在这个场景中依赖的就是order对象(所需请求参数)。
package com.gs.designmodel.ordermodel.orderfood;
import java.util.Map;
import java.util.Set;
/**
* @author: Gaos
* @Date: 2023-01-16 13:57
*
* 订单命令类, 属于具体的命令类, 需要聚合 对象接收者和接收者依赖的操作数据
**/
public class FoodCommand implements Command{
/**
* 持有接收者对象
*/
private SeniorChef receiver;
/**
* 订单,依赖者需要接受的数据
*/
private Order order;
public FoodCommand(SeniorChef receiver, Order order) {
this.receiver = receiver;
this.order = order;
}
@Override
public void execute() {
System.out.println(order.getDiningTable() + "桌的订单");
Map<String, Integer> foodDir = order.getFoodDir();
Set<String> keys = foodDir.keySet();
for (String item : keys) {
receiver.makeFood(item, foodDir.get(item));
}
System.out.println(order.getDiningTable() + "桌的订单已制作完毕!!!!!");
}
@Override
public void undo() {
}
}
服务员类:
调用者(触发者)的角色
package com.gs.designmodel.ordermodel.orderfood;
import java.util.ArrayList;
import java.util.List;
/**
* @author: Gaos
* @Date: 2023-01-16 14:02
*
* 服务员类 (调用者或请求者角色)
**/
public class Waitor {
/**
* 持有多个命令对象
*/
private List<Command> commands = new ArrayList<>();
public void setCommand(Command command) {
commands.add(command);
}
/**
* 发动命令功能, (其实是给大厨下订单的过程)
*/
public void orderUp() {
System.out.println("服务员 : 有新的订单了");
for (Command item : commands) {
if(item != null) {
item.execute();
}
}
}
}
测试:
public static void main(String[] args) {
//创建第一个订单对象
Order order1 = new Order();
order1.setDiningTable(1);
order1.setFood("西红柿鸡蛋面",1);
order1.setFood("小杯可乐",2);
//创建第二个订单对象
Order order2 = new Order();
order2.setDiningTable(2);
order2.setFood("尖椒肉丝盖饭",1);
order2.setFood("小杯雪碧",1);
//创建厨师对象
SeniorChef receiver = new SeniorChef();
//创建命令对象
FoodCommand cmd1 = new FoodCommand(receiver,order1);
FoodCommand cmd2 = new FoodCommand(receiver,order2);
//创建调用者(服务员对象)
Waitor invoke = new Waitor();
invoke.setCommand(cmd1);
invoke.setCommand(cmd2);
//让服务员发起命令
invoke.orderUp();
}
结果:
服务员 : 有新的订单了
1桌的订单
1份西红柿鸡蛋面
2份小杯可乐
1桌的订单已制作完毕!!!!!
2桌的订单
1份尖椒肉丝盖饭
1份小杯雪碧
2桌的订单已制作完毕!!!!!
假如我们现在需要扩展,除了大厨需要做饭,还需要歌手去唱歌。那我们就需要一个唱歌命令类去实现抽象命令类,需要一个歌手(接收者)实现具体的执行逻辑,其他的则不需要变化。在唱歌命令类中持有歌手的引用对象,从而实现调用操作的对象与实现该操作的对象解耦。
在命令模式中也可以很好的实现对操作的撤销和更改,因为存储的有每次命令的记录,可以通过实现 undo()
方法来进行实现。
同时也可以实现宏命令,也就是将多个命令组合成一个组合命令。
4.扩展
其实我们常常使用的JDK
中的Runnable
就是一个典型的命令模式。
其中 Runnable
可以理解为抽象命令类,所有实现此接口的类都需要实现 run
方法。
我们都知道创建线程的方式之一就是实现 Runnable
接口。
@Slf4j
class SomeRunnable implements Runnable{
@Override
public void run() {
log.info("通过实现Runnable接口创建线程, {}", Thread.currentThread().getName());
}
}
这个时候其实 SomeRunnable
类就是具体的命令类,类比上面的 FoodCommand
类。
public static void main(String[] args) {
SomeRunnable someRunnable = new SomeRunnable();
Thread thread1 = new Thread(someRunnable);
thread1.start();
}
然后我们会将其丢入到 Thread
中 调用 start()
方法开启线程,去执行我们写在 run()
方法中的逻辑。
所以其实 Thread
类充当的就是调用者的角色,只是在这种情况下,没有具体的接受者角色了,或者说接受者角色和具体命令实现类耦合在了一起。
总结一下,Runnable
就是担当的命令的角色,Thread
充当的是调用者, 实现了 Runnable
的类就是具体的命令类, start()
方法就是其执行方法。
5.总结
如果你需要通过操作来参数化对象, 可使用命令模式。 如果你想要实现操作回滚功能, 可使用命令模式。(可能需要结合其他的设计模式)
它可以在在不修改已有客户端代码的情况下在程序中创建新的命令,同时解耦触发和执行操作的类,支持实现撤销和恢复,以及操作的延迟执行。同时你也可以将一系列命令组合成为一个复杂命令(宏命令)。
但是代码可能会更加的复杂,因为你其实是在发送者和接收者之间增加了一个新的层级。类也会更加的多,因为需要有很多的具体命令类。
相关参考文章:
可以的话希望大家多多点赞、收藏一下哈!