写在前面的话
复习、总结23种设计模式
上一篇
命令模式(行动(Action)模式或交易(Transaction)模式)
记重点
拿餐厅点餐为例,客人需要向服务员发出请求,但完全不知道做菜的厨师的姓名和联系方式,
也不知道厨师炒菜的步骤。但客人点菜后,便会有固定的厨师完成做菜的任务。
命令模式就会把客人订餐请求封装为Command对象,传到厨师手中。这就是实现了客人和厨师的耦合关系。
涉及到的角色:客人、服务员、厨师
动作:客人点菜、交给服务员、服务员让厨师做菜
服务员:接收来自客人的命令1(让厨师做土豆饭)
接收来自客人的命令2(让厨师做牛肉饭)
客户端Client完全不需要知道厨师做饭的细节,只需要知道自己点什么饭就可以了
定义
将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
有时候需要向某些对象发送请求,但不知道请求的接收者是谁,也不知道被请求的具体操作是什么,此时通过一种松耦合的方式来设计,使得请求发送者和请求接收者消除彼此的耦合关系。
拿餐厅点餐为例,客人需要向服务员发出请求,但完全不知道做菜的厨师的姓名和联系方式,也不知道厨师炒菜的步骤。但客人点菜后,便会有固定的厨师完成做菜的任务。命令模式就会把客人订餐请求封装为Command对象,传到厨师手中。这就是实现了客人和厨师的耦合关系。
命令模式涉及五个角色:
命令接口(Command):定义执行命令的方法,可以是抽象类或接口。
具体命令类(Concrete Command):实现命令接口,封装了具体的请求和接收者,负责执行请求。
接收者类(Receiver):执行实际的操作,命令对象将请求委托给接收者来执行。
命令执行者(Invoker):调用命令对象来执行请求,并负责命令的管理和控制。
客户端(Client):创建具体的命令对象,并将其分配给调用者来执行。
命令模式的应用场景是当需要将请求发送者和请求接收者解耦时,例如在需要对请求排队、记录日志、撤销操作等情况下2。
- 客户端(Client)角色: 创建一个具体命令(ConcreteCommand)对象并确定其接收者。
- 命令(Command)角色: 声明了一个给所有具体命令类的抽象接口。
- 具体命令(ConcreteCommand)角色: 定义一个接收者和行为之间的弱耦合;实现execute()方法,负责调用接收者的相应操作。execute()方法通常叫做执行方法。
- 请求者(Invoker)角色: 负责调用命令对象执行请求,相关的方法叫做行动方法。
- 接收者(Receiver)角色: 负责具体实施和执行一个请求。任何一个类都可以成为接收者,实施和执行请求的方法叫做行动方法。
命令模式的实现方式
实现刚才点菜的案例
- 命令(Command)角色: Command
package com.design.pattern.command.test02;
// 点餐命令
public interface Command {
void execute();
}
- 具体命令(ConcreteCommand)角色: PotatoCommand、BeefCommand
package com.design.pattern.command.test02;
// 点土豆饭
public class PotatoCommand implements Command {
private Chef chef;
public PotatoCommand(Chef chef) {
this.chef = chef;
}
// 交给对应的厨师做
@Override
public void execute() {
this.chef.cookPotato();
}
}
package com.design.pattern.command.test02;
// 点牛肉饭
public class BeefCommand implements Command {
private Chef chef;
public BeefCommand(Chef chef) {
this.chef = chef;
}
// 交给对应的厨师做
@Override
public void execute() {
this.chef.cookBeef();
}
}
- 请求者(Invoker)角色:Waiter
package com.design.pattern.command.test02;
// 服务员
public class Waiter {
private Command command;
public Waiter(Command command) {
this.command = command;
}
// 服务员下单,执行做饭命令
public void order() {
this.command.execute();
}
}
- 接收者(Receiver)角色:Cook
package com.design.pattern.command.test02;
// 具体的厨师类
public class Chef {
public void cookPotato() {
System.out.println("已完成土豆饭");
}
public void cookBeef() {
System.out.println("已完成牛肉饭");
}
}
- 客户端(Client)角色: Client
package com.design.pattern.command.test02;
/**
* 拿餐厅点餐为例,客人需要向服务员发出请求,但完全不知道做菜的厨师的姓名和联系方式,
* 也不知道厨师炒菜的步骤。但客人点菜后,便会有固定的厨师完成做菜的任务。
* 命令模式就会把客人订餐请求封装为Command对象,传到厨师手中。这就是实现了客人和厨师的耦合关系。
* <p>
* 涉及到的角色:客人、服务员、厨师
* 动作:客人点菜、交给服务员、服务员让厨师做菜
* <p>
* 服务员:接收来自客人的命令1(让厨师做土豆饭)
* 接收来自客人的命令2(让厨师做牛肉饭)
* 客户端Client完全不需要知道厨师做饭的细节,只需要知道自己点什么饭就可以了
*/
public class Client {
public static void main(String[] args) {
Chef chef = new Chef();
// chef.cookBeef();
BeefCommand beefCommand = new BeefCommand(chef);
Waiter waiter = new Waiter(beefCommand);
waiter.order();
PotatoCommand potatoCommand = new PotatoCommand(chef);
Waiter waiter1 = new Waiter(potatoCommand);
waiter1.order();
}
}
// 执行结果
已完成牛肉饭
已完成土豆饭
以上就是完整实现的代码客户端Client完全不需要知道厨师做饭的细节,只需要知道自己点什么饭就可以了,但现实中,客户可以点套餐,也可以取消某个饭,如何实现呢?
package org.commandPattern.version2;
import java.util.ArrayList;
import java.util.List;
/**
* 点餐命令
*/
abstract class Command{
public String name;
public Cook cook;
public Command(Cook cook, String name){
this.name = name;
this.cook = cook;
}
// 具体如何执行,交给实现类
public abstract void execute() throws InterruptedException;
public String getName(){
return this.name;
}
}
/**
* 土豆饭命令
*/
class PotatoCommand extends Command {
public PotatoCommand(Cook cook,String name){
super(cook,name);
}
@Override
public void execute() throws InterruptedException {
super.cook.cookPotato();
}
@Override
public String getName() {
return name;
}
}
/**
* 牛肉饭命令
*/
class BeefCommand extends Command {
public BeefCommand(Cook cook,String name){
super(cook,name);
}
@Override
public void execute() throws InterruptedException {
super.cook.cookBeef();
}
@Override
public String getName() {
return name;
}
}
/**
* 厨师
*/
class Cook{
public boolean cookPotato() throws InterruptedException {
Thread.sleep(2000);
System.out.println("已完成土豆饭");
return true;
}
public boolean cookBeef() throws InterruptedException {
Thread.sleep(2000);
System.out.println("已完成牛肉饭");
return true;
}
}
/**
* 服务员
*/
class Waiter extends Thread {
// 单点某个饭
private Command command;
// 点多个饭,例如套餐,可以用队列实现,更加方便
private List<Command> commands = new ArrayList<>();
public Waiter(){}
public Waiter(Command command){
this.command = command;
}
// 下单
public void order() throws InterruptedException {
this.command.execute();
}
// 添加菜
public void addCommand(Command command){
System.out.println("客户点餐:" + command.getName());
this.commands.add(command);
}
// 取消菜
public void removeCommand(Command command){
// 如果菜还没做,就移除
if(commands.contains(command)){
System.out.println("客户取消:" + command.getName());
this.commands.remove(command);
}else {
System.out.println("取消失败,厨师已经在做了");
}
}
// 套餐
public void setMeal() throws InterruptedException {
int len = commands.size();
int i;
for (i = 0; i < len; i++){
// 逐个取出命令
if (commands.size() > 0) {
Command c = commands.get(i);
// 执行命令
c.execute();
// 执行完的命令进行移除
commands.remove(c);
i--;
}
}
}
@Override
public void run() {
try {
setMeal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public class Client {
public static void main(String[] args) throws InterruptedException {
Cook cook = new Cook();
BeefCommand beefCommand = new BeefCommand(cook,"牛肉饭");
PotatoCommand potatoCommand = new PotatoCommand(cook,"土豆饭");
Waiter waiter = new Waiter();
waiter.addCommand(beefCommand);
waiter.addCommand(potatoCommand);
waiter.start();
Thread.sleep(10000);
// 客户突然不想要了,要把菜退掉
waiter.removeCommand(potatoCommand);
}
}
// 运行结果如下
客户点餐:牛肉饭
客户点餐:土豆饭
已完成牛肉饭
已完成土豆饭
取消失败,厨师已经在做了
上面代码完成了套餐功能,而且可以取消点的饭,非常人性化。
命令模式的优点
- 更松散的耦合:命令模式使得发起命令的对象——客户端,和具体实现命令的对象——接收者对象完全解耦,也就是说发起命令的对象完全不知道具体实现对象是谁,也不知道如何实现。
- 更动态的控制:命令模式把请求封装起来,可以动态地对它进行参数化、队列化和日志化等操作,从而使得系统更灵活。
- 很自然的复合命令:命令模式中的命令对象能够很容易地组合成复合命令,也就是宏命令,从而使系统操作更简单,功能更强大。
- 更好的扩展性:由于发起命令的对象和具体的实现完全解耦,因此扩展新的命令就很容易,只需要实现新的命令对象,然后在装配的时候,把具体的实现对象设置到命令对象中,然后就可以使用这个命令对象,已有的实现完全不用变化。
命令模式的缺点
- 命令类的数量可能会增加:由于每个命令都需要创建一个具体命令类,因此命令模式可能会导致类的数量增加,这可能会导致代码更加复杂,使得系统更难以维护。
- 可能会增加系统的复杂性:使用命令模式需要创建多个类和对象,并需要在它们之间建立关系,这可能会增加系统的复杂性,使得代码更难以理解和维护。
- 可能会导致性能下降:由于每个命令都需要创建一个具体命令对象,并将其传递给调用者,因此命令模式可能会导致系统的性能下降,特别是在处理大量请求时。
命令模式结合其他模式会更优秀:命令模式可以结合责任链模式,实现命令族解析任务;结合模板方法模式,将多个Command动作封装到模版方法,可以减少 Command子类的膨胀问题
JavaSE命令模式的应用
java.lang.Runnable使用了命令模式,线程的start()方法之后,就有资格去争抢CPU资源,而不需要编写获得CPU资源的逻辑。而线程抢到CPU资源后,就会执行run()方法中的内容,用Runnable接口把用户请求和CPU执行进行解耦。
首先先了解下Ruunable的使用
package org.commandPattern.version2;
class Writer{
public void write(int i){
System.out.println("线程[" + i + "]正在调用我写入数据");
}
}
class MyRunnable implements Runnable{
private int name;
private Writer writer;
public MyRunnable(int name,Writer writer){
this.name = name;
this.writer = writer;
}
@Override
public void run() {
System.out.println("线程[" + name + "]正在运行");
this.writer.write(name);
}
}
public class TestRunnable {
public static void main(String[] args) {
for(int i = 0; i < 5; i++){
Writer writer = new Writer();
MyRunnable r = new MyRunnable(i,writer);
Thread t = new Thread(r);
t.start();
}
}
}
// 运行结果
线程[0]正在运行
线程[0]正在调用我写入数据
线程[1]正在运行
线程[1]正在调用我写入数据
线程[2]正在运行
线程[2]正在调用我写入数据
线程[4]正在运行
线程[4]正在调用我写入数据
线程[3]正在运行
线程[3]正在调用我写入数据
代码很简单,不用多说,实现Runnable接口,传入Thread就可以实现多线程运行。分析下该模式
- 客户端(Client)角色: Client
- 命令(Command)角色:Runnable
- 具体命令(ConcreteCommand)角色: MyRunnable,JDK的SoftAudioPusher、PipeWriter也是
- 请求者(Invoker)角色:Thread
- 接收者(Receiver)角色:要执行的动作,自定义类Writer,例如SoftAudioPusher的AudioInputStream
SoftAudioPusher类的内如如下
public final class SoftAudioPusher implements Runnable {
private volatile boolean active = false;
private SourceDataLine sourceDataLine = null;
private Thread audiothread;
private final AudioInputStream ais;
private final byte[] buffer;
public SoftAudioPusher(SourceDataLine var1, AudioInputStream var2, int var3) {
this.ais = var2;
this.buffer = new byte[var3];
this.sourceDataLine = var1;
}
// 省略部分代码
public void run() {
byte[] var1 = this.buffer;
AudioInputStream var2 = this.ais;
SourceDataLine var3 = this.sourceDataLine;
try {
while(this.active) {
// 真正调用接收者(Receiver)的read方法
int var4 = var2.read(var1);
if (var4 < 0) {
break;
}
// 真正调用接收者(Receiver)write方法
var3.write(var1, 0, var4);
}
} catch (IOException var5) {
this.active = false;
}
}
// 省略部分代码
}
Struts2命令模式的应用
在Struts2的Action调用中使用了命令模式,我们来回顾下Action的调用过程
调用的过程为:StrutsActionProxy.execute()->DefaultActionInvocation.invoke()->xxInterceptor.intercept()-自定义Action类
在ActionProxy中持有ActionInvocation对象,ActionProxy的execute方法负责调用DefaultActionInvocation的invoke方法。
分析下各个类在命令模式中的角色
- 客户端(Client)角色:Dispatcher
- 命令(Command)角色:ActionInvocation
- 具体命令(ConcreteCommand)角色:DefaultActionInvocation
- 请求者(Invoker)角色:StrutsActionProxy
- 接收者(Receiver)角色:LoginAction(自定义Action,也就是业务Action)
对比下两个原型图,是不是非常标准的命令模式,第一张为标准命令模式图,第二个为Struts2中的命令模式图,如果要学习UML的知识可以参考:zhuanlan.zhihu.com/p/109655171
UML知识图如下,UML的知识可以参考:zhuanlan.zhihu.com/p/109655171