一天一种JAVA设计模式之十八:命令模式

182 阅读10分钟

写在前面的话

复习、总结23种设计模式

获取详细源码请点击我

上一篇

# 一天一种JAVA设计模式之十七:责任链模式

命令模式(行动(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);
    }
}
// 运行结果如下
客户点餐:牛肉饭
客户点餐:土豆饭
已完成牛肉饭
已完成土豆饭
取消失败,厨师已经在做了

上面代码完成了套餐功能,而且可以取消点的饭,非常人性化。

命令模式的优点

  1. 更松散的耦合:命令模式使得发起命令的对象——客户端,和具体实现命令的对象——接收者对象完全解耦,也就是说发起命令的对象完全不知道具体实现对象是谁,也不知道如何实现。
  2. 更动态的控制:命令模式把请求封装起来,可以动态地对它进行参数化、队列化和日志化等操作,从而使得系统更灵活。
  3. 很自然的复合命令:命令模式中的命令对象能够很容易地组合成复合命令,也就是宏命令,从而使系统操作更简单,功能更强大。
  4. 更好的扩展性:由于发起命令的对象和具体的实现完全解耦,因此扩展新的命令就很容易,只需要实现新的命令对象,然后在装配的时候,把具体的实现对象设置到命令对象中,然后就可以使用这个命令对象,已有的实现完全不用变化。

命令模式的缺点

  1. 命令类的数量可能会增加:由于每个命令都需要创建一个具体命令类,因此命令模式可能会导致类的数量增加,这可能会导致代码更加复杂,使得系统更难以维护。
  2. 可能会增加系统的复杂性:使用命令模式需要创建多个类和对象,并需要在它们之间建立关系,这可能会增加系统的复杂性,使得代码更难以理解和维护。
  3. 可能会导致性能下降:由于每个命令都需要创建一个具体命令对象,并将其传递给调用者,因此命令模式可能会导致系统的性能下降,特别是在处理大量请求时。

命令模式结合其他模式会更优秀:命令模式可以结合责任链模式,实现命令族解析任务;结合模板方法模式,将多个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

参考# Java设计模式-命令模式(Command Pattern)