命令模式

思考命令模式

命令模式就是通过将请求封装成对象,实现请求的发送者和接收者之间的解耦,同时支持队列操作、日志记录和撤销操作。

1.命令模式的本质

命令模式的本质:封装请求。

命令模式的本质是将请求封装成一个对象,使得可以用不同的请求对客户端进行参数化,支持队列操作、日志记录和撤销操作,并且能够实现请求的发送者和接收者之间的解耦。

什么是将请求封装成一个对象,使得可以用不同的请求对客户端进行参数化?

例如下面的厨师做菜案例, 我们可以定义一个抽象的命令接口 Command,其中包含一个执行方法 execute()。然后,我们可以创建具体的命令类,如 FishCommand(做鱼香肉丝的命令) 和 MeatCommand(做北京烤鸭的命令),每个类封装了不同的请求操作。

客户端可以根据需要创建不同的命令对象,并将这些命令对象传递给请求者对象(如菜单)。这样,不同的请求操作可以通过命令对象的参数化来实现,而不需要直接调用具体的请求操作。

2.何时选用命令模式

建议在以下情况时选用命令模式。

  • 当需要将请求操作的调用者和接收者解耦时,以便能够灵活地对它们进行变化和扩展。
  • 当需要对请求进行参数化,使得可以在不同的请求之间进行切换和组合,从而实现动态的请求操作。
  • 当需要支持命令的撤销、恢复、排队、日志记录等特性时。
  • 当需要实现任务调度、异步处理、队列操作等场景时。
  • 当需要构建具有嵌套命令结构的系统,以支持复杂的操作和组合。

典型场景:

  • 菜单和按钮操作:在图形用户界面中,可以将不同的菜单项和按钮操作封装成具体的命令对象,并将其与相应的请求操作关联。这样,可以轻松地添加、修改和组合不同的命令,实现灵活的界面交互。

  • 任务调度和异步处理:命令模式可以用于实现任务调度器,将任务封装成命令对象并按照需要进行调度和执行。同时,可以支持异步处理和任务队列,以提高系统的并发性和性能。

  • 撤销和恢复操作:命令模式可以记录命令的执行历史,使得可以轻松地实现撤销和恢复操作。每个命令对象可以保存执行所需的状态和参数,从而可以在需要时撤销执行操作,或者重新执行先前的操作。

  • 日志记录和审计功能:由于每个命令对象都封装了执行操作的细节和参数,可以很容易地记录和存储命令对象。这样可以实现日志记录和审计功能,用于跟踪和监控系统的操作。

  • 复杂操作和组合:命令模式支持构建具有嵌套命令结构的系统,以支持复杂的操作和组合。可以将多个命令对象组合成一个复合命令,从而实现更高层次的操作和控制。

3.优缺点

命令模式的优点

  • 解耦调用者和接收者
    命令模式将请求操作封装成命令对象,使得调用者和接收者之间解耦,调用者无需知道具体的接收者,只需调用命令对象即可,从而提高了系统的灵活性和可维护性。

  • 容易扩展新的命令
    由于命令模式将请求操作封装成命令对象,因此非常容易添加新的命令类,而无需修改现有的代码,符合开闭原则。

  • 支持队列操作和撤销操
    命令模式可以将命令对象放入队列中,实现命令的排队、延迟和调度等操作。同时,由于命令对象封装了操作的状态和参数,可以实现命令的撤销和恢复。

  • 支持日志记录和审计
    由于命令对象封装了请求操作的细节和参数,可以很容易地记录和存储命令对象,实现日志记录和审计功能。

缺点。

  • 类膨胀
    使用命令模式会增加代码量,因为每个具体的命令都需要一个单独的类来实现。如果命令非常多,可能会导致类的数量剧增,增加系统的复杂性。
  • 可能引入过多的细粒度命令对象
    在一些简单的场景下,命令模式可能会引入过多的细粒度命令对象,增加了系统的开销。

4.命令模式的结构

在这里插入图片描述

  • Command:定义命令的接口,声明执行的方法。
  • ConcreteCommand:命令接口实现对象,是“虚”的实现;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
  • Receiver:接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
  • Invoker:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
  • Client:创建具体的命令对象,并且设置命令对象的接收者。注意这个不是我们常规意义上的客户端,而是在组装命令对象和接收者,或许,把这个Client称为装配者会更好理解,因为真正使用命令的客户端是从Invoker来触发执行。

5.实现

耦合写法

模拟电脑开机,点击机箱开机按钮,调用主板初始化系统,然后用户就能操作了

1.主板类

/**
 * @description:主板接口
 */
public interface MainBoardApi {

    /**
     * 开机
     */
    void open();
}

/**
 * @description:技嘉主板
 */
public class JiJiaMainBoard implements MainBoardApi{

    @Override
    public void open() {
        System.out.println("技嘉主板正在开机,请稍后");
        System.out.println("接通电源.............");
        System.out.println("设备检查.............");
        System.out.println("装载系统.............");
        System.out.println("机器正常运行,请操作....");
    }
}

/**
 * @description:微星主板
 */
public class WeiXinMainBoard implements MainBoardApi{

    @Override
    public void open() {
        System.out.println("微星主板正在开机,请稍后");
        System.out.println("接通电源.............");
        System.out.println("设备检查.............");
        System.out.println("装载系统.............");
        System.out.println("机器正常运行,请操作....");
    }
}

2.开机按钮类

/**
 * @description:机箱开机按钮
 */
public class BoxButton {

    /**
     * 点击开机按钮,就开机
     */
    public void boot(int flag){
        if (1==flag){
            new JiJiaMainBoard().open();
        }else {
            new WeiXinMainBoard().open();
        }
    }
}

3.测试类

/**
 * @description:最开始耦合写法
 */
public class Test1 {

    public static void main(String[] args) {
        //点击按钮开机
        new BoxButton().boot(2);
    }
}

现在,机箱按钮直接调用主板,是强耦合的关系,很不利于维护
在这里插入图片描述

命令模式优化耦合写法

改造上面的耦合写法,主板接口和实现类不变

1.调用程序类(也就是上面的机箱按钮)

/**
 * @description:调用程序(机箱开机按钮)
 */
public class Invoker {

    /**
     * 持有命令对象
     */
    private Command command=null;

    public void setCommand(Command command) {
        this.command = command;
    }

    /**
     * 开机
     */
    public void boot(){
        command.execute();
    }
}

2.命令类

/**
 * @description:命令接口
 */
public interface Command {

    /**
     * 执行命令
     */
    void execute();
}

/**
 * @description:具体命令类(这里是开机命令)
 */
@AllArgsConstructor
public class ConcreteCommand implements Command{

    /**
     * 持有主板对象
     */
    private MainBoardApi mainBoardApi;

    @Override
    public void execute() {
        //命令类不能进行开机操作
        //调用主板进行开机
        mainBoardApi.open();
    }
}

3.测试类

/**
 * @description:测试类
 * @createTime 2022/11/30 13:06
 */
public class Client {

    public static void main(String[] args) {
        //把命令和实现组装起来
        Command command=new ConcreteCommand(new WeiXinMainBoard());
        //为机箱按钮设置命令
        Invoker invoker = new Invoker();
        invoker.setCommand(command);
        //模拟开机按钮
        invoker.boot();
    }
}

4.效果
在这里插入图片描述

在这里插入图片描述

命令模式实现撤销

撤销有两种:

  • 补偿式(反操作式)
  • 存储恢复式

模拟假如存在一个存在一个撤销按钮,电脑开机后,点击撤销按钮,撤销开机操作,也就是进行关机

1.主板类增加关机功能

/**
 * @description:主板接口
 */
public interface MainBoardApi {

    /**
     * 开机
     */
    void open();

    /**
     * 关机
     */
    void close();
}

/**
 * @description:技嘉主板
 */
public class JiJiaMainBoard implements MainBoardApi{

    @Override
    public void open() {
        System.out.println("技嘉主板正在开机,请稍后");
        System.out.println("接通电源.............");
        System.out.println("设备检查.............");
        System.out.println("装载系统.............");
        System.out.println("机器正常运行,请操作....");
    }

    @Override
    public void close() {
        System.out.println("技嘉主板正在关机,请稍后");
        System.out.println("关机成功.............");
    }
}

/**
 * @description:微星主板
 */
public class WeiXinMainBoard implements MainBoardApi{

    @Override
    public void open() {
        System.out.println("微星主板正在开机,请稍后");
        System.out.println("接通电源.............");
        System.out.println("设备检查.............");
        System.out.println("装载系统.............");
        System.out.println("机器正常运行,请操作....");
    }

    @Override
    public void close() {
        System.out.println("微星主板正在关机,请稍后");
        System.out.println("关机成功.............");
    }
}

2.命令接口增加撤销命令

/**
 * @description:命令接口
 */
public interface Command {

    /**
     * 执行命令
     */
    void execute();

    /**
     * 撤销命令
     */
    void undo();
}

/**
 * @description:具体命令类(这里是开机命令)
 */
@AllArgsConstructor
public class ConcreteCommand implements Command{

    /**
     * 持有主板对象
     */
    private MainBoardApi mainBoardApi;

    @Override
    public void execute() {
        //命令类不能进行开机操作
        //调用主板进行开机
        mainBoardApi.open();
    }

    @Override
    public void undo() {
        //撤销开机,也就是关机
        mainBoardApi.close();
    }
}

3.调用程序增加关机功能

/**
 * @description:调用程序(机箱开机按钮)
 */
public class Invoker {

    /**
     * 持有命令对象
     */
    private Command command=null;

    public void setCommand(Command command) {
        this.command = command;
    }

    /**
     * 开机
     */
    public void boot(){
        command.execute();
    }

    /**
     * 关机
     */
    public void shutdown(){
        command.undo();
    }
}

4.测试类

public class Client {

    public static void main(String[] args) {
        //把命令和实现组装起来
        Command command=new ConcreteCommand(new WeiXinMainBoard());
        //为机箱按钮设置命令
        Invoker invoker = new Invoker();
        invoker.setCommand(command);
        //模拟开机按钮
        invoker.boot();

        //模拟点击撤销按钮,电脑关机
        invoker.shutdown();
    }
}

5.结果
在这里插入图片描述

命令模式实现厨师做菜

模拟两个厨师,一个做热菜,一个做凉菜,服务员点菜
在这里插入图片描述

1.厨师类

/**
 * @description:厨师接口
 */
public interface CookApi {

    /**
     * 做菜
     * @param name
     */
    void cook(String name);
}

/**
 * @description:热菜
 */
public class HotCook implements CookApi{
    @Override
    public void cook(String name) {
        System.out.println("厨师正在做:"+name);
    }
}

/**
 * @description:凉菜
 */
public class CoolCook implements CookApi{
    @Override
    public void cook(String name) {
        System.out.println("厨师正在做:"+name);
    }
}

2.命令类

/**
 * @description:命令接口
 */
public interface Command {

    /**
     * 执行命令
     */
    void execute();
}

/**
 * @description:生鱼刺身
 */
public class FishCommand implements Command{

    private CookApi cookApi=null;

    public void setCookApi(CookApi cookApi) {
        this.cookApi = cookApi;
    }

    @Override
    public void execute() {
        cookApi.cook("生鱼刺身");
    }
}

/**
 * @description:北京烤鸭
 */
public class MeatCommand implements Command{

    private CookApi cookApi=null;

    public void setCookApi(CookApi cookApi) {
        this.cookApi = cookApi;
    }

    @Override
    public void execute() {
        cookApi.cook("北京烤鸭");
    }
}

3.菜单类

/**
 * @description:菜单
 */
public class Menu{

    private Collection<Command> menu=new ArrayList<>();

    /**
     * 点菜,将菜品加入菜单
     * @param cmd
     */
    public void addCommand(Command cmd){
        menu.add(cmd);
    }

    public void execute() {
        //遍历菜单,做菜
        for (Command cmd:menu){
            cmd.execute();
        }
    }
}

4.服务员(调用程序)

/**
 * @description:调用程序(服务员)
 */
public class Waiter {

    private Menu menu=new Menu();

    /**
     * 客人点菜
     * @param cmd
     */
    public void orderDish(Command cmd){
        //判断菜品是热菜还是凉菜
        if (cmd instanceof FishCommand){
            ((FishCommand)cmd).setCookApi(new HotCook());
        }else {
            ((MeatCommand)cmd).setCookApi(new CoolCook());
        }
        //加到菜单中
        menu.addCommand(cmd);
    }

    /**
     * 点菜完毕,执行命令去做菜
     */
    public void orderOver(){
        menu.execute();
    }
}

5.测试类

public class Client {

    public static void main(String[] args) {
        Waiter waiter = new Waiter();

        //点菜
        waiter.orderDish(new FishCommand());
        waiter.orderDish(new MeatCommand());

        //点菜完毕
        waiter.orderOver();
    }
}

6.结果
在这里插入图片描述

命令模式实现排队

在这里插入图片描述
修改代码如下

1.测试类

public class Client {

    public static void main(String[] args) {

        //创建3位厨师
        HotCook cook1 = new HotCook("张三");
        HotCook cook2 = new HotCook("李四");
        HotCook cook3 = new HotCook("王五");

        //启动线程
        new Thread(cook1).start();
        new Thread(cook2).start();
        new Thread(cook3).start();

        //模拟10桌客人
        for (int i=0;i<10;i++){
            Waiter waiter = new Waiter();
            //每个客人都点了北京烤鸭和生鱼刺身
            waiter.orderDish(new FishCommand(i));
            waiter.orderDish(new MeatCommand(i));
            //点菜完毕
            waiter.orderOver();
        }
    }
}

2.服务员类

/**
 * @description:调用程序(服务员)
 */
public class Waiter {

    private Menu menu =new Menu();

    /**
     * 客人点菜
     * @param cmd
     */
    public void orderDish(Command cmd){
        //加到菜单中
        menu.addCommand(cmd);
    }

    /**
     * 点菜完毕,执行命令去做菜
     */
    public void orderOver(){
        menu.execute();
    }
}

3.菜单类

/**
 * @description:菜单
 */
public class Menu {

    private Collection<Command> menu=new ArrayList<>();

    /**
     * 点菜,将菜品加入菜单
     * @param cmd
     */
    public void addCommand(Command cmd){
        menu.add(cmd);
    }

    /**
     * 获取菜单中所有命令
     * @return
     */
    public Collection<Command> getCommands(){
        return this.menu;
    }

    public void execute() {
        //将菜单传给后厨
        CommandQueue.addMenu(this);
    }
}

4.后厨管理类(队列)

/**
 * @description:菜单队列
 */
public class CommandQueue {

    private static List<Command> cmds=new ArrayList<>();

    /**
     * 将菜单中做菜的命令取出来,加入一个队列
     * @param menu
     */
    public synchronized static void addMenu(Menu menu){
        for (Command cmd:menu.getCommands()){
            cmds.add(cmd);
        }
    }

    /**
     * 按顺序取出一个命令
     * @return
     */
    public synchronized static Command getOneCommand(){
        Command cmd=null;
        if (cmds.size()>0){
            //按顺序取出第一个命令
            cmd=cmds.get(0);
            //取完就移除
            cmds.remove(0);
        }
        return cmd;
    }
}

5.命令类

/**
 * @description:命令接口
 */
public interface Command {

    /**
     * 执行命令
     */
    void execute();

    /**
     * 设置命令接收者
     * @param cookApi
     */
    void setCookApi(CookApi cookApi);
}

/**
 * @description:生鱼刺身
 */
public class FishCommand implements Command{

    private CookApi cookApi=null;

    /**
     * 桌号
     */
    private int tableId;

    @Override
    public void setCookApi(CookApi cookApi) {
        this.cookApi = cookApi;
    }

    public FishCommand(int tableId) {
        this.tableId = tableId;
    }

    @Override
    public void execute() {
        cookApi.cook(tableId,"生鱼刺身");
    }
}

/**
 * @description:北京烤鸭
 */
public class MeatCommand implements Command{

    private CookApi cookApi=null;

    /**
     * 桌号
     */
    private int tableId;

    @Override
    public void setCookApi(CookApi cookApi) {
        this.cookApi = cookApi;
    }

    public MeatCommand(int tableId) {
        this.tableId = tableId;
    }

    @Override
    public void execute() {
        cookApi.cook(tableId,"北京烤鸭");
    }
}

6.厨师类,暂时只用了一个实现类,可以不使用接口

/**
 * @description:厨师接口
 */
public interface CookApi {

    /**
     * 做菜
     * @param tableId 桌号
     * @param name
     */
    void cook(int tableId,String name);
}

/**
 * @description:热菜
 */
@AllArgsConstructor
public class HotCook implements CookApi,Runnable{
    private String name;

    @Override
    public void cook(int tableId, String name) {
        //做菜时间
        int cookTime=(int)(20*Math.random());
        System.out.println(this.name+"厨师正在做"+tableId+"号桌的热菜:"+name);

        try {
            Thread.sleep(cookTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.name+"厨师做好了"+tableId+"号桌的热菜:"+name+",共耗时"+cookTime);
    }

    @Override
    public void run() {
        while (true){
            //从后厨取一个做菜命令
            Command oneCommand = CommandQueue.getOneCommand();
            if (oneCommand!=null){
                //设置自己为做菜厨师
                oneCommand.setCookApi(this);
                //执行命令
                oneCommand.execute();
            }
            //做完菜休息1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
命令模式实现日志持久化

在这里插入图片描述

1.FishCommand和MeatCommand实现序列化接口

public class MeatCommand implements Command, Serializable {}

public class FishCommand implements Command, Serializable {}

2.序列化和反序列化工具类

/**
 * @description:文件操作类
 */
public class FileUtil {

    /**
     * 读文件,反序列化
     * @param pathName
     * @return
     */
    public static List readFile(String pathName){
        List list=new ArrayList();
        ObjectInputStream oin=null;
        File file = new File(pathName);
        if (file.exists()){
            try {
                oin=new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)));
                //反序列化
                list= (List) oin.readObject();
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                if (oin!=null){
                    try {
                        oin.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return list;
    }

    /**
     * 写入文件,序列化
     * @param pathName
     * @param list
     */
    public static void writeFile(String pathName,List list){
        File file = new File(pathName);
        ObjectOutputStream oos=null;
        try {
            oos=new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
            //序列化
            oos.writeObject(list);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (oos!=null){
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.后厨管理类

/**
 * @description:菜单队列
 */
public class CommandQueue {

    private final static String FILE_NAME="C:\\Users\\Lenovo\\Desktop\\commandqueue.txt";

    private static List<Command> cmds=null;

    /**
     * 静态代码块,每次重启先加载
     */
    static {
        cmds= FileUtil.readFile(FILE_NAME);
        if (cmds==null){
            cmds=new ArrayList<>();
        }
    }

    /**
     * 将菜单中做菜的命令取出来,加入一个队列
     * @param menu
     */
    public synchronized static void addMenu(Menu menu){
        for (Command cmd:menu.getCommands()){
            cmds.add(cmd);
        }
        //覆盖写入文件
        FileUtil.writeFile(FILE_NAME,cmds);
    }

    /**
     * 按顺序取出一个命令
     * @return
     */
    public synchronized static Command getOneCommand(){
        Command cmd=null;
        if (cmds.size()>0){
            //按顺序取出第一个命令
            cmd=cmds.get(0);
            //取完就移除
            cmds.remove(0);
            //覆盖写入文件
            FileUtil.writeFile(FILE_NAME,cmds);
        }
        return cmd;
    }
}

执行
在这里插入图片描述

在第八桌的时候终止程序,然后再重启看效果,会从上次中断的地方继续做菜,然后再开始新得做菜
在这里插入图片描述

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值