【浅谈设计模式】(15): 命令模式--小妖怪的夏天

4,672 阅读5分钟

我正在参加「掘金·启航计划」

前言

⛱ 小猪妖有个梦想,那就是离开浪浪山,出去闯荡,

但是在短篇当中小猪妖并没有走出浪浪山,和我们普通人一样,都走不出自己的浪浪山。

我知道浪浪山的那边还是山,可我还是想去看看。

--《中国奇谭》

一、入门

1.1 概述

将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。

命令模式的一个核心就是 "解耦" 。

那是谁发生了耦合呢?

答:请求发起者和请求接收者的耦合。典型的例子就是餐厅点餐,由顾客点好菜单,这个菜单就是"命令",然后 "接收者" 厨师开始制作;倘若没有解耦,那就是由顾客直接跟厨师交代事情。命令模式非常像消息队列,有消息生产者、消费者、以及消息命令。

本文以《小妖怪的夏天》中的场景来理解下命令模式。

大王想吃唐僧肉,于是开了场项目立项大会,将这个命令分发下去,教头又把命令分发给不同的小弟,对于大王来说,它只需要关心消息发送下去了,而接收到消息的小妖怪们,就开始执行不同的命令,有烧水的、有制作箭头的、有砍菜的~

命令模式是一种数据驱动的设计模式,将请求命令封装成一个命令对象,由调用者将这个命令对象发给执行者。

1.2 结构

命令模式包含以下主要角色:

  • 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
  • 具体命令(Concrete  Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
  • 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
  • 调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。

二、案例实战

对小妖怪的案例做个代码演示。

实现者角色(无名的小妖怪),也可以定义一个抽象类,继承实现。

/**
 * 一只无名的小妖怪
 * @Author xiaolei
 * @Date 2023/2/28 13:19
 **/
public class Nobody {

    /**
     * 执行烧水工作
     */
    public void execBoilWater(){
        System.out.println("大王让我来烧水!");
    }

    /**
     * 执行制箭工作
     */
    public void execMakeArrow(){
        System.out.println("大王让我做1000支箭!");
    }
}

抽象命令接口

/**
 * 小妖怪抽象命令接口
 * @Author xiaolei
 * @Date 2023/2/28 13:12
 **/
public interface GoblinCommand {
    void execute();
}

具体命令角色

/**
 * 烧水具体命令
 * 具体命令中需要传入一个具体干活的对象
 * @Author xiaolei
 * @Date 2023/2/28 13:23
 **/
public class BoilWaterComman implements GoblinCommand {

    private Nobody nobody;

    /**
     * 这里的 nobody 也可以抽象出一个抽象类
     * @param nobody
     */
    public BoilWaterComman(Nobody nobody){
        this.nobody = nobody;
    }
    /**
     * 该命令只负责让接收者干烧水的事情
     */
    @Override
    public void execute() {
        nobody.execBoilWater();
    }
}

// 制作箭头具体命令
public class MakeArrowCommand implements GoblinCommand{
    private Nobody nobody;

    /**
     * 这里的 nobody 也可以抽象出一个抽象类
     * @param nobody
     */
    public MakeArrowCommand(Nobody nobody){
        this.nobody = nobody;
    }
    /**
     * 该命令只负责让接收者干制箭的事情
     */
    @Override
    public void execute() {
        nobody.execMakeArrow();
    }
}

调用者角色 (大王)

/**
 * 大王 : 命令的发起者。
 * 注意它是跟命令直接交互的,而命令是跟干活的直接交互的,所以需要传入 命令
 * the king of the forest
 * 森林之王
 * @Author xiaolei
 * @Date 2023/2/28 13:28
 **/
public class TheKingInvoker {
    private GoblinCommand command;

    /**
     * 根据命令创建调用者
     * @param command
     */
    public TheKingInvoker(GoblinCommand command){
        this.command = command;
    }
    /**
     * 大王开始吩咐命令
     */
    public void action(){
        command.execute();
    }
}

客户端测试

public class ClientTest {
    public static void main(String[] args) {
        // 小妖怪上线
        Nobody nobody = new Nobody();
        BoilWaterComman boilWaterComman = new BoilWaterComman(nobody);
        // 大王吩咐执行烧水命令
        TheKingInvoker theKingInvoker = new TheKingInvoker(boilWaterComman);
        // 开始执行
        theKingInvoker.action();
        System.out.println("===================================");
        MakeArrowCommand makeArrowCommand = new MakeArrowCommand(nobody);
        // 大王吩咐执行制箭命令
        TheKingInvoker theKingInvoker2 = new TheKingInvoker(makeArrowCommand);
        // 开始执行
        theKingInvoker2.action();
    }
}

三、总结

3.1 使用场景

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
  • 系统需要在不同的时间指定请求、将请求排队和执行请求。
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。

3.2 优缺点

优点:

  • 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
  • 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  • 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

缺点:

  • 使用命令模式可能会导致某些系统有过多的具体命令类。
  • 系统结构更加复杂。

命令模式的入门就到这了,感觉命令模式在业务的使用应该很少,可能在重构的时候会考虑吧,一句话就是以数据驱动来完成调用者与执行者的解耦工作。

我想离开浪浪山,那你要变得更强呀~~