从零开始学习Java设计模式 | 行为型模式篇:命令模式
Posted 李阿昀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 行为型模式篇:命令模式相关的知识,希望对你有一定的参考价值。
在本讲,我们来学习一下行为型模式里面的第三个设计模式,即命令模式。
概述
首先,我们先来看下这样一个场景:在日常生活中,我们出去吃饭都会遇到下面的场景。
顾客把订单交给女服务员,女服务员拿到这个订单之后放在订单柜台,然后喊一声:“订单来了!”,厨师拿到这个订单之后就开始准备餐点。
这里我们来思考一个问题,如果真要去实现以上这样一个场景的话,那么又该如何来实现呢?
大家想一想,服务员要下单的话,那么她是不是得把单下给某一个厨师呀!所以,要是按照之前的做法,那就是在服务员对象里面创建一个厨师对象,然后服务员下单的话就相当于是调用厨师对象中的方法进行餐点准备。但是,这样做的话,服务员对象和厨师对象就耦合在一起了,而这便会导致一个问题,就是如果后期餐馆要发展扩大,想要把原有的厨师换掉,改换另外一个厨师,那么此时你会发现服务员对象里面的代码也需要进行一个修改,而这就违背开闭原则了。
出现以上问题之后,我们又应该如何来解决呢?这时,我们就可以使用命令模式了。那什么是命令模式呢?下面我们就来看看它的概念。
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
看完上面命令模式的概念,一时半会看不懂咋办呢?我们可以结合一开始的例子来理解。
命令模式是说将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这里,我们结合一开始的例子可以这样来理解,发出请求的就是服务员对象,而执行请求的则是厨师对象,依据命令模式的概念,现在我们所要做的就是将厨师和服务员这俩对象进行分割,不能让它们耦合在一起。
这样,它们两者之间则通过命令对象进行沟通,服务员下命令,具体是谁去做,咱们不用过多的去关注,厨师对象拿到这个命令之后,开始准备餐点就行,而且具体是哪个服务员下的单,咱们也不需要去关注,这其实也是使用命令模式的一个好处。
理解了命令模式的概念之后,接下来,我们再来看看命令模式的结构,也就是说命令模式包含哪些角色。
结构
命令模式包含以下主要角色:
-
抽象命令类(Command)角色:定义命令的接口,声明执行的方法。
-
具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
命令下达之后,肯定要明确到底是由谁来执行,那么到底由谁来执行呢?接收者,例如一开始的例子中的厨师就属于接收者。
-
实现者/接收者(Receiver)角色:接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。也就是说,人人都可以成为厨师,只要他能做饭。
-
调用者/请求者(Invoker)角色:要求命令对象执行请求,通常会持有命令对象,并且可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
如果请求者(或者调用者)要下发命令,那么它就得持有命令对象。注意了,上面例子中的服务员就属于请求者(或者调用者)。而且,请求者(或者调用者)可以持有很多的命令对象,这是因为一个服务员她可以下多个订单。此外,请求者(或者调用者)还相当于是使用命令对象的入口,也就是说服务员就是命令模式里面的入口,客户下单之后,总得由服务员去下发命令,是不是啊!
命令模式案例
接下来,我们就要编写代码实现以上案例了,通过该案例大家再去好好理解一下命令模式。
分析
将上面的案例用代码实现,那么我们就需要分析出命令模式里面的角色在该案例中到底是由谁来充当的了。
首先,来看一下服务员她充当的是什么角色?服务员充当的是调用者角色,是由她来发起命令的,给谁发送命令呢?给厨师发送命令。
然后,来看一下厨师他充当的是什么角色?厨师充当的是接收者角色,真正命令执行的对象。
接着,再来看一下订单。它不是真正的命令,只不过命令中包含了订单。因为服务员发出命令之后,她得告诉厨师要去做什么样的一个餐品,所以命令里面就包含了订单。
搞清楚上面三个概念之后,下面我们再来看看这样一张类图。
以上类图里面有很多类,而且它们的关系也比较复杂,下面我们一个一个来说说。
可以看到,有一个叫做SeniorChef的类,它就是厨师类,该类里面有一个制作食物的功能,而且该功能中有两个参数,分别表示食物的份数和名称。
然后,再来看一下叫做Order的类,它是订单类,该类里面有两个属性,分别是餐桌号和所点食物,而且在该类中我们还为这俩属性提供了对应的getter和setter方法。此外,大家一定注意了,所点食物是由一个Map集合来表示的,该Map集合里面的键其实就是食物的名称,而值就是食物的份数。
接着,我们再来看一下叫做Command的接口,它充当的是抽象命令类角色,里面只有一个方法,即execute(执行)。而且,它下面还有一个子实现类,即OrderCommand,该类里面聚合进来了厨师类变量和订单类变量。在该类里面,除了重写父接口里面的execute方法之外,它还提供了一个有参的构造方法为聚合进来的俩变量赋值。
总之,大家一定要搞清楚OrderCommand类,它既实现了Command接口,又聚合了订单以及厨师对象。
最后,我们再来看一下叫做Waitor的类,它是服务员类,它里面聚合了一个Command接口的List集合。为什么会这样呢?这是因为一个服务员她可以发出多个命令啊!
而且,从上可以看到,在该类里面,我们提供了两个方法:一个是setCommand,它是用来将Command接口类型的对象存储到List集合中去的;一个是orderUp,它是用来发出命令的。
当然了,以上类图里面还有一个客户端类,即Client,该类我们就不用过多的去关注它了,我们主要关注的是上面5个类或者接口,以及它们之间的关系。
实现
上面我们分析了一下点餐案例里面所涉及到的类,以及类和类之间的关系。接下来,我们就要编写代码来实现该案例了。
首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即command,也即命令模式的具体代码我们是放在了该包下。
然后,创建订单类,这里我们就起名为Order了。
package com.meimeixia.pattern.command;
import java.util.HashMap;
import java.util.Map;
/**
* 订单类
* @author liayun
* @create 2021-08-03 14:29
*/
public class Order {
// 餐桌号码
private int diningTable;
// 所下的餐品及份数
private Map<String, Integer> foodDic = new HashMap<String, Integer>();
public int getDiningTable() {
return diningTable;
}
public void setDiningTable(int diningTable) {
this.diningTable = diningTable;
}
public Map<String, Integer> getFoodDic() {
return foodDic;
}
public void setFood(String name, int num) {
foodDic.put(name, num);
}
}
接着,创建命令模式里面的接收者,即厨师类,这里我们就起名为SeniorChef了。
package com.meimeixia.pattern.command;
/**
* 厨师类
* @author liayun
* @create 2021-08-03 14:34
*/
public class SeniorChef {
// 制作食物的功能
public void makeFood(String name, int num) {
System.out.println(num + "份" + name); // 这儿输出的是多少份什么食物
}
}
紧接着,创建命令模式里面的抽象命令类,这里我们是将其定义成了一个接口,名字就叫Command。
package com.meimeixia.pattern.command;
/**
* 抽象命令类
* @author liayun
* @create 2021-08-03 14:37
*/
public interface Command {
void execute(); // 只需要定义一个统一的执行方法
}
再紧接着,创建命令模式里面的具体命令类,即以上Command接口的子实现类,这里我们就起名为OrderCommand了。
package com.meimeixia.pattern.command;
import java.util.Map;
import java.util.Set;
/**
* 具体的命令类
* @author liayun
* @create 2021-08-03 14:53
*/
public class OrderCommand implements Command {
// 具体命令类通常会持有接收者对象
private SeniorChef receiver;
// 此外,具体命令类还得持有订单对象,因为要让厨师去做菜,还得告诉他需要做哪些菜
private Order order;
// 提供一个有参构造方法为以上两个成员变量赋值
public OrderCommand(SeniorChef receiver, Order order) {
this.receiver = receiver;
this.order = order;
}
@Override
public void execute() {
System.out.println(order.getDiningTable() + "桌的订单:");
// 发送命令,让厨师去做订单里面的菜...
Map<String, Integer> foodDic = order.getFoodDic();
// 遍历Map集合
Set<String> keys = foodDic.keySet();
for (String foodName : keys) {
receiver.makeFood(foodName, foodDic.get(foodName));
}
System.out.println(order.getDiningTable() + "桌的饭准备完毕!!!");
}
}
具体命令类创建完毕之后,接下来,我们创建命令模式里面的请求者,即服务员类,这里我们就起名为Waitor了。
package com.meimeixia.pattern.command;
import java.util.ArrayList;
import java.util.List;
/**
* 服务员类(属于请求者角色)
* @author liayun
* @create 2021-08-03 15:14
*/
public class Waitor {
// 请求者可以持有多个命令对象,这是因为一个服务员她可以下多个订单,即发出多个命令
private List<Command> commands = new ArrayList<Command>();
public void setCommand(Command cmd) {
// 将cmd对象存储到List集合中
commands.add(cmd);
}
// 发起命令的功能。这儿,服务员只需喊一声订单来了就行,然后厨师就开始去执行
public void orderUp() {
System.out.println("美女服务员:叮咚,大厨,新订单来了......");
// 遍历List集合
for (Command command : commands) {
if (command != null) {
command.execute();
}
}
}
}
最后,创建一个客户端类用于测试。
package com.meimeixia.pattern.command;
/**
* @author liayun
* @create 2021-08-03 15:31
*/
public class Client {
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();
// 将订单和接收者封装成命令对象
OrderCommand cmd1 = new OrderCommand(receiver, order1);
OrderCommand cmd2 = new OrderCommand(receiver, order2);
// 创建调用者,即服务员对象
Waitor invoke = new Waitor();
// 设置命令
invoke.setCommand(cmd1);
invoke.setCommand(cmd2);
// 让服务员发起命令,即将订单带到柜台,并向厨师喊:订单来了
invoke.orderUp();
}
}
此时,运行以上客户端类,打印结果如下图所示,可以看到确实是我们所想要的结果。
至此,以上点餐案例我们就做完了。做是做完了,但是大家不觉得现在这个系统结构变得越来越复杂了吗?是不是有这样一个感受啊!嘻嘻😘
命令模式的优缺点以及使用场景
接下来,我们就来看一下命令模式的优缺点以及使用场景。
优缺点
优点
关于命令模式的优点,我总结出了下面四点。
-
降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
说人话,降低的是谁和谁之间的耦合度啊?拿上面案例来说,降低的是调用者和接收者这两者之间的耦合度。
-
增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,即它满足"开闭原则",对扩展比较灵活。
也就是说,后期如果我们想要增加一个具体的命令的话,那么只需要再定义一个类就可以了,而不必再去改原有的代码。
-
可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,而这个组合命令我们就可以将其称为宏命令。当然了,上述案例中还未涉及到宏命令。
-
方便实现Undo和Redo操作。命令模式可以与后面我们即将要学习的备忘录模式结合,实现命令的撤销与恢复。
什么是Undo,什么又是Redo呢?Undo指的是命令的撤销,例如下了一个订单之后,我现在又不想下了,那么这时系统就得支持撤销操作了;Redo指的是命令的恢复,例如我现在又想再次下单,那么这时系统就得支持恢复操作了。
缺点
关于命令模式的缺点,我总结出了下面两点。
- 使用命令模式可能会导致某些系统有过多的具体命令类。但是大家一定要清楚,它并不会导致类爆炸这种现象发生。
- 系统结构更加复杂。通过上述案例,相信大家能感受到我们的整个系统架构变得更加复杂了,相应地,这对程序员的要求就变得比较高了。
使用场景
只要出现如下几个场景,我们就可以去考虑一下能不能使用命令模式了。
-
系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
这里,我还得强调一下,是调用者和接收者之间解除耦合。我为什么得强调这一点呢?因为我们之前学习过很多设计模式,它们都可以实现解除耦合,但是大家要注意解除耦合的角色是不一样的。
-
系统需要在不同的时间指定请求、将请求排队和执行请求。
这说的是啥意思啊?这里我稍微给大家解释解释,回到上述案例中,对于请求者来说,它里面用了一个List集合来存储多个命令对象,而在orderUp方法里面去执行命令时,我们是不是得遍历List集合啊,既然是遍历List集合,那么是不是得按照一个顺序进行遍历啊,这也就是说,谁先下单,那谁的餐就先做好,这便是将请求排队。
-
系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
目前,撤销操作和恢复操作我们并没有实现,因为命令模式得和备忘录模式结合之后才能实现命令的撤销与恢复。
命令模式在JDK源码中的应用
接下来,我们就来看一看命令模式在JDK源码里面是如何来应用的。
相信大家对Runable这个接口并不陌生,这里我要说的是该接口就用到了一个典型的命令模式。下面我就来说一下该命令模式里面角色的一个划分。
Runnable担当的是抽象命令角色,因为它是一个接口。
// 命令接口(抽象命令角色)
public interface Runnable {
public abstract void run();
}
Thread充当的是调用者角色,start方法就是其执行方法。
// 调用者
public class Thread implements Runnable {
private Runnable target;
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
}
从上可以看到,在Thread类的成员位置声明了一个Runable类型的变量,这是因为我们之前说过,调用者里面通常会持有命令对象。声明完这么一个变量之后,我们还得为其设置值,至于如何设置,我在这里就不做具体的分析了。
此外,在Thread类里面还有一个start方法,当咱们通过调用者对象去调用该start方法时,实质上就是去执行命令对象里面的run方法。而且,如果你仔细查看start方法的源码,那么你会发现它里面又调用了一个native方法,即start0,该方法是调用系统资源来开启一个线程的。
接下来,再来看一下我创建的具体命令类。
/**
* jdk Runnable 命令模式
* TurnOffThread:属于具体命令角色
*/
public class TurnOffThread implements Runnable{
private Receiver receiver;
public TurnOffThread(Receiver receiver) {
this.receiver = receiver;
}
public void run() {
receiver.turnOFF();
}
}
可以看到,在TurnOffThread类的成员位置声明了一个Receiver类型的变量,想必大家都知道了,Receiver类充当的就是接收者角色,由于接收者是对程序员开放的,也就是说可以由开发者自己去定义接收者,所以大家可以自行去定义,只不过在这里我没有定义出来而已!
而且,之前我们也说过,具体命令类里面通常会持有接收者对象,这就是为何我们要在TurnOffThread类的成员位置声明一个Receiver类型的变量的原因。
此外,我们还能看到,TurnOffThread类除了重写命令接口里面的run方法之外,还提供了一个有参构造为Receiver类型的成员变量赋值。而且,大家还要注意,重写的run方法里面是直接去调用接收者对象里面的方法来进行具体的业务逻辑处理的。
以上就是我们分析出来的应用在JDK源码里面的命令模式的那些角色,相信大家这会还并不陌生。
当然了,最后还应该有一个测试类,如下所示。
/**
* 测试类
*/
public class Demo {
public static void main(String[] args) {
Receiver receiver = new Receiver();
TurnOffThread turnOffThread = new TurnOffThread(receiver);
Thread thread = new Thread(turnOffThread);
thread.start();
}
}
这里我就不运行以上测试类给大家看打印结果了,因为我们还没有定义出Receiver类(即接收者)来。
至此,大家体会到了命令模式在JDK源码里面的具体应用了吧!
以上是关于从零开始学习Java设计模式 | 行为型模式篇:命令模式的主要内容,如果未能解决你的问题,请参考以下文章