杰瑞科技汇

Java命令模式如何实现与使用?

什么是命令模式?

命令模式是一种行为型设计模式,它将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

Java命令模式如何实现与使用?-图1
(图片来源网络,侵删)

核心思想: 将“发出请求的对象”(调用者/Invoker)与“接收并执行请求的对象”(接收者/Receiver)解耦,调用者不需要知道接收者是谁,也不需要知道具体如何执行请求,它只需要调用一个命令对象的方法即可。


命令模式的四大核心角色

为了更好地理解,我们用一个生活中的例子点外卖来类比。

角色 类比(点外卖) 在设计模式中的定义
Command (命令接口) “订单”的规范 定义了执行操作的接口,通常是 execute() 方法。
ConcreteCommand (具体命令) 一份具体的“川菜订单”或“粤菜订单” 实现了 Command 接口,持有对接收者对象的引用,并调用接收者的相应操作来执行请求。
Invoker (调用者/请求者) “顾客”或“手机App” 要求命令对象执行请求,它会调用命令对象的 execute() 方法,但它本身并不知道命令的具体内容或接收者是谁。
Receiver (接收者) “川菜馆”或“粤菜馆” 知道如何执行与请求相关的具体业务逻辑,是真正干活的对象。

还有一个非常重要的角色: | Client (客户端) | 创建订单的人 | 创建具体命令对象,并设定它的接收者。 |


代码实现(点外卖例子)

下面我们用 Java 代码来实现这个点外卖的例子。

Java命令模式如何实现与使用?-图2
(图片来源网络,侵删)

第1步:定义 Receiver (接收者) - 餐厅

这是真正执行做饭动作的对象。

// Receiver.java - 接收者
public class Restaurant {
    private String dishName;
    public Restaurant(String dishName) {
        this.dishName = dishName;
    }
    // 真正执行做饭的动作
    public void cook() {
        System.out.println(dishName + " 餐厅正在为您精心烹饪...");
    }
}

第2步:定义 Command (命令接口)

// Command.java - 命令接口
public interface Order {
    // 执行命令的方法
    void execute();
}

第3步:定义 ConcreteCommand (具体命令)

这个命令对象会持有一个接收者(餐厅)的引用,并调用它的 cook() 方法。

// SichuanDishOrder.java - 具体命令
public class SichuanDishOrder implements Order {
    private Restaurant restaurant; // 持有接收者的引用
    public SichuanDishOrder(Restaurant restaurant) {
        this.restaurant = restaurant;
    }
    @Override
    public void execute() {
        restaurant.cook(); // 调用接收者的方法来执行具体操作
    }
}

第4步:定义 Invoker (调用者) - 顾客/App

顾客不关心是哪家餐厅做菜,他只知道下一个订单。

// Customer.java - 调用者
public class Customer {
    private Order order; // 持有一个命令对象的引用
    // 设置要下的订单
    public void setOrder(Order order) {
        this.order = order;
    }
    // 下单,执行订单
    public void placeOrder() {
        if (order != null) {
            order.execute();
        } else {
            System.out.println("您还没有选择菜品!");
        }
    }
}

第5步:定义 Client (客户端) - 创建并组装对象

// Client.java - 客户端
public class DeliveryApp {
    public static void main(String[] args) {
        // 1. 创建接收者 (餐厅)
        Restaurant sichuanRestaurant = new Restaurant("麻婆豆腐");
        Restaurant cantoneseRestaurant = new Restaurant("白切鸡");
        // 2. 创建具体命令对象,并设定它的接收者
        Order sichuanOrder = new SichuanDishOrder(sichuanRestaurant);
        Order cantoneseOrder = new SichuanDishOrder(cantoneseRestaurant); // 注意:这里也可以用另一个具体命令类
        // 3. 创建调用者 (顾客)
        Customer customer = new Customer();
        // 4. 客户端将命令对象传递给调用者
        customer.setOrder(sichuanOrder);
        customer.placeOrder(); // 输出: 麻婆豆腐 餐厅正在为您精心烹饪...
        System.out.println("--------------------");
        // 5. 可以动态地更换命令
        customer.setOrder(cantoneseOrder);
        customer.placeOrder(); // 输出: 白切鸡 餐厅正在为您精心烹饪...
    }
}

运行结果

麻婆豆腐 餐厅正在为您精心烹饪...
--------------------
白切鸡 餐厅正在为您精心烹饪...

通过这个例子,你可以清晰地看到:

Java命令模式如何实现与使用?-图3
(图片来源网络,侵删)
  • Customer (Invoker) 完全不知道 Restaurant (Receiver) 的存在。
  • Customer 只和 Order (Command) 打交道,它调用 order.execute()
  • SichuanDishOrder (ConcreteCommand) 连接了 CustomerRestaurant,它知道如何让 Restaurant 做饭。
  • 这就是解耦的核心体现。

命令模式的优点和缺点

优点

  1. 解耦:这是命令模式最大的优点,调用者和接收者完全解耦,使得调用者无需知道接收者的任何信息。
  2. 可扩展性强:可以很容易地引入新的命令,而无需修改调用者或现有接收者的代码。
  3. 支持组合命令:可以将多个命令组合成一个宏命令(Composite Command),一次性执行一系列操作。
  4. 支持撤销操作:通过在 Command 接口中增加 undo() 方法,并在具体命令中实现它,可以轻松实现撤销功能。
  5. 支持请求排队和日志:可以将命令对象放入队列中,实现异步执行,也可以将命令对象持久化,实现请求日志。

缺点

  1. 可能导致类数量过多:对于每一个具体的操作,都需要创建一个具体的命令类,如果系统中有大量操作,可能会导致类的数量急剧增加。
  2. 增加系统的复杂性:引入了新的抽象层(Command接口),对于非常简单的场景,可能会显得小题大做。

命令模式的应用场景

命令模式在以下场景中非常有用:

  1. GUI按钮和菜单项:每个按钮或菜单项都是一个命令,点击按钮就是调用命令的 execute() 方法,这使得你可以轻松地改变按钮的行为,只需给它一个新的命令对象即可。
  2. 事务系统:在数据库操作中,可以将一系列操作封装成命令,如果执行成功,则提交;如果失败,则可以调用 undo() 方法回滚。
  3. 多线程任务队列:生产者将任务(命令对象)放入队列,消费者从队列中取出任务并执行。
  4. 撤销/重做 功能:几乎所有支持撤销功能的软件(如Photoshop, Word)都使用了命令模式,每次操作都会创建一个命令对象,并将其放入历史记录栈中,撤销时,只需从栈中弹出上一个命令并调用其 undo() 方法即可。

增强版:支持撤销的命令模式

我们稍微修改一下 Order 接口和 SichuanDishOrder 来支持撤销。

// 增强的命令接口
public interface Order {
    void execute();
    void undo(); // 新增撤销方法
}
// 增强的具体命令
public class SichuanDishOrder implements Order {
    private Restaurant restaurant;
    public SichuanDishOrder(Restaurant restaurant) {
        this.restaurant = restaurant;
    }
    @Override
    public void execute() {
        restaurant.cook();
    }
    @Override
    public void undo() {
        System.out.println(restaurant.getDishName() + " 订单已取消,餐厅停止烹饪。");
    }
}
// 在Customer中增加撤销方法
public class Customer {
    // ... 其他代码不变
    private Order lastOrder; // 记录上一个订单
    public void placeOrder() {
        if (order != null) {
            lastOrder = order; // 记录当前订单为上一个订单
            order.execute();
        } else {
            System.out.println("您还没有选择菜品!");
        }
    }
    public void cancelLastOrder() {
        if (lastOrder != null) {
            lastOrder.undo();
            lastOrder = null;
        } else {
            System.out.println("没有可撤销的订单。");
        }
    }
}
// 在main方法中测试
public static void main(String[] args) {
    Restaurant restaurant = new Restaurant("水煮鱼");
    Order order = new SichuanDishOrder(restaurant);
    Customer customer = new Customer();
    customer.setOrder(order);
    customer.placeOrder(); // 输出: 水煮鱼 餐厅正在为您精心烹饪...
    System.out.println("--------------------");
    customer.cancelLastOrder(); // 输出: 水煮鱼 订单已取消,餐厅停止烹饪。
}

与其他模式的比较

  • 命令模式 vs 策略模式

    • 相似点:两者都将算法/行为封装成对象。
    • 不同点:策略模式关注的是封装可互换的算法,使得算法可以独立于使用它的客户端而变化,命令模式关注的是封装一个请求,以便将其排队、记录日志或支持撤销,策略模式的 Context 通常会直接调用 Strategy 的方法,而命令模式的 InvokerReceiver 是完全解耦的。
  • 命令模式 vs 适配器模式

    • 两者都包装一个对象,但适配器模式是为了接口不兼容的两个对象之间建立桥梁,而命令模式是为了将请求的发送者和接收者解耦

命令模式是一个非常强大的模式,它的核心价值在于解耦封装请求,虽然可能会增加一些类的数量,但它带来的灵活性、可扩展性和对高级功能(如撤销、宏命令)的支持,使其在构建复杂、灵活的系统时成为一个不可或缺的工具,在Java中,java.lang.Runnable 接口就可以看作是一个命令模式的应用,run() 方法就是 execute() 方法。

分享:
扫描分享到社交APP
上一篇
下一篇