命令模式:设计与实践

27 阅读13分钟

命令模式:设计与实践

一、什么是命令模式

1. 基本定义

命令模式(Command Pattern)是一种行为型设计模式,由《设计模式:可复用面向对象软件的基础》(GOF著作)定义为:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作

该模式通过将“请求”封装为独立的命令对象,实现请求发送者(调用者)与请求接收者(执行者)的解耦,使两者无需知道对方的存在。核心是将“做什么”与“怎么做”分离,支持请求的排队、日志记录、撤销等扩展功能。

2. 核心思想

命令模式的核心在于请求的对象化封装。当系统中存在多种请求(如支付、退款、资金划拨),且需要对这些请求进行排队、记录日志、撤销或重试时,通过定义统一的命令接口,将每种请求封装为独立的命令对象,命令对象包含执行请求所需的全部信息(参数、接收者、执行逻辑),从而使请求可以像普通对象一样被传递、存储和管理,提升系统的灵活性和可扩展性。

二、命令模式的特点

1. 请求封装为对象

将每个请求封装为独立的命令对象,命令对象包含执行请求的所有信息。

2. 调用者与接收者解耦

调用者只需触发命令的执行方法,无需知道接收者的具体实现,接收者也无需知道调用者的存在。

3. 支持扩展功能

天然支持请求的排队、日志记录、撤销、重试等功能,无需修改原有业务逻辑。

4. 命令可组合

多个命令可以组合为复合命令,实现复杂操作的批量执行。

5. 增加类的数量

每个请求都需要对应一个命令类,可能导致系统中类的数量增加。

特点说明
请求封装为对象每个请求被封装为命令对象,包含执行所需的全部信息
调用者与接收者解耦调用者与接收者通过命令对象间接交互,互不依赖
支持扩展功能支持请求排队、日志、撤销、重试等扩展操作
命令可组合多个命令可组合为复合命令,实现批量操作
增加类的数量每个请求对应一个命令类,可能增加系统复杂度

三、命令模式的标准代码实现

1. 模式结构

命令模式包含四个核心角色:

  • 命令接口(Command):定义命令的执行方法(如execute())和可选的撤销方法(如undo())。
  • 具体命令(ConcreteCommand):实现命令接口,封装具体请求,包含接收者引用和执行所需参数。
  • 调用者(Invoker):负责触发命令的执行,持有命令对象的引用,不直接与接收者交互。
  • 接收者(Receiver):实际执行业务逻辑的对象,知道如何完成命令要求的操作。

2. 代码实现示例

2.1 命令接口
/**
 * 命令接口
 * 定义命令的执行和撤销方法
 */
public interface Command {
    /**
     * 执行命令
     */
    void execute();

    /**
     * 撤销命令(可选)
     */
    void undo();
}
2.2 接收者
/**
 * 接收者:文件操作服务
 * 实际执行业务逻辑
 */
public class FileService {
    /**
     * 创建文件
     */
    public void createFile(String fileName) {
        System.out.println("创建文件:" + fileName);
    }

    /**
     * 删除文件
     */
    public void deleteFile(String fileName) {
        System.out.println("删除文件:" + fileName);
    }
}
2.3 具体命令
/**
 * 具体命令:创建文件命令
 */
public class CreateFileCommand implements Command {
    // 接收者引用
    private final FileService fileService;
    // 命令参数
    private final String fileName;

    public CreateFileCommand(FileService fileService, String fileName) {
        this.fileService = fileService;
        this.fileName = fileName;
    }

    @Override
    public void execute() {
        // 调用接收者的方法执行命令
        fileService.createFile(fileName);
    }

    @Override
    public void undo() {
        // 撤销命令:删除已创建的文件
        fileService.deleteFile(fileName);
    }
}

/**
 * 具体命令:删除文件命令
 */
public class DeleteFileCommand implements Command {
    private final FileService fileService;
    private final String fileName;

    public DeleteFileCommand(FileService fileService, String fileName) {
        this.fileService = fileService;
        this.fileName = fileName;
    }

    @Override
    public void execute() {
        fileService.deleteFile(fileName);
    }

    @Override
    public void undo() {
        // 撤销命令:重新创建已删除的文件
        fileService.createFile(fileName);
    }
}
2.4 调用者
import java.util.ArrayList;
import java.util.List;

/**
 * 调用者:命令处理器
 * 负责触发命令执行和管理命令历史
 */
public class CommandInvoker {
    // 当前命令
    private Command currentCommand;
    // 命令历史记录(用于撤销)
    private final List<Command> commandHistory = new ArrayList<>();

    /**
     * 设置并执行命令
     */
    public void executeCommand(Command command) {
        this.currentCommand = command;
        command.execute();
        commandHistory.add(command);
    }

    /**
     * 撤销上一个命令
     */
    public void undoLastCommand() {
        if (!commandHistory.isEmpty()) {
            Command lastCommand = commandHistory.remove(commandHistory.size() - 1);
            lastCommand.undo();
        }
    }
}
2.5 客户端使用示例
/**
 * 客户端
 * 使用命令模式
 */
public class Client {
    public static void main(String[] args) {
        // 1. 创建接收者
        FileService fileService = new FileService();

        // 2. 创建命令
        Command createCommand = new CreateFileCommand(fileService, "order.txt");
        Command deleteCommand = new DeleteFileCommand(fileService, "order.txt");

        // 3. 创建调用者
        CommandInvoker invoker = new CommandInvoker();

        // 4. 执行命令
        System.out.println("=== 执行创建命令 ===");
        invoker.executeCommand(createCommand);

        System.out.println("\n=== 执行删除命令 ===");
        invoker.executeCommand(deleteCommand);

        System.out.println("\n=== 撤销上一个命令 ===");
        invoker.undoLastCommand();
    }
}

3. 代码实现特点总结

角色核心职责代码特点
命令接口(Command)定义命令的执行和撤销方法声明execute()方法,可选undo()方法,统一命令调用标准
具体命令(ConcreteCommand)实现命令接口,封装请求持有接收者引用和执行参数,execute()方法调用接收者的对应方法
调用者(Invoker)触发命令执行,管理命令持有命令引用,提供executeCommand()方法,可记录命令历史支持撤销
接收者(Receiver)实际执行业务逻辑包含具体业务方法,不知道命令的存在,只负责完成具体操作

四、支付框架设计中命令模式的运用

支付操作的日志与审计为例,说明命令模式在支付系统中的具体实现:

1. 场景分析

支付系统中,关键操作(如资金划拨、退款、限额调整)需要满足以下要求:

  • 详细记录操作日志(操作人、时间、参数、结果),支持审计追踪
  • 操作失败时支持重试,确保数据一致性
  • 避免操作的重复执行(如防止重复划拨资金)
  • 操作过程可追溯,便于问题排查

使用命令模式可将每个支付操作封装为命令对象,命令对象包含操作所需的全部信息(参数、接收者、日志数据),调用者负责触发命令执行并管理日志,实现操作与日志审计的解耦。

2. 设计实现

2.1 命令接口与日志模型
import java.time.LocalDateTime;

/**
 * 可审计的命令接口
 */
public interface AuditableCommand {
    /**
     * 执行命令
     * @return 是否执行成功
     */
    boolean execute();

    /**
     * 获取命令ID
     */
    String getCommandId();

    /**
     * 获取操作日志
     */
    OperationLog getOperationLog();
}

/**
 * 操作日志模型
 */
public class OperationLog {
    private String commandId;
    private String operator; // 操作人
    private LocalDateTime operateTime;
    private String type; // 操作类型
    private String params; // 操作参数
    private String result; // 执行结果

    // getter和setter方法...
}
2.2 接收者服务
import java.math.BigDecimal;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * 资金服务(接收者)
 * 实际处理资金划拨业务
 */
public class FundService {
    // 模拟账户余额
    private final ConcurrentMap<String, BigDecimal> accounts = new ConcurrentHashMap<>();

    public FundService() {
        // 初始化测试账户
        accounts.put("ACC001", new BigDecimal("10000.00"));
        accounts.put("ACC002", new BigDecimal("5000.00"));
    }

    /**
     * 资金划拨(支持幂等性)
     * @param commandId 命令ID(用于幂等校验)
     * @param fromAccount 转出账户
     * @param toAccount 转入账户
     * @param amount 金额
     * @return 是否成功
     */
    public boolean transferWithIdempotency(String commandId, String fromAccount, 
                                         String toAccount, BigDecimal amount) {
        // 1. 幂等性校验:检查该命令是否已执行
        if (isCommandExecuted(commandId)) {
            return true; // 已执行过,直接返回成功
        }

        // 2. 余额校验
        BigDecimal fromBalance = accounts.getOrDefault(fromAccount, BigDecimal.ZERO);
        if (fromBalance.compareTo(amount) < 0) {
            return false; // 余额不足
        }

        // 3. 执行划拨
        accounts.put(fromAccount, fromBalance.subtract(amount));
        accounts.put(toAccount, accounts.getOrDefault(toAccount, BigDecimal.ZERO).add(amount));

        // 4. 记录已执行的命令(防止重复执行)
        markCommandExecuted(commandId);
        return true;
    }

    // 模拟幂等性存储(实际使用数据库或Redis)
    private ConcurrentMap<String, Boolean> executedCommands = new ConcurrentHashMap<>();
    private boolean isCommandExecuted(String commandId) {
        return executedCommands.containsKey(commandId);
    }
    private void markCommandExecuted(String commandId) {
        executedCommands.put(commandId, true);
    }

    // 获取账户余额(用于测试)
    public BigDecimal getBalance(String account) {
        return accounts.getOrDefault(account, BigDecimal.ZERO);
    }
}
2.3 具体命令实现
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;

/**
 * 资金划拨命令(具体命令)
 */
public class FundTransferCommand implements AuditableCommand {
    private final String commandId;
    private final String fromAccount;
    private final String toAccount;
    private final BigDecimal amount;
    private final FundService fundService; // 接收者
    private final OperationLog log;

    public FundTransferCommand(String fromAccount, String toAccount, BigDecimal amount,
                              FundService fundService, String operator) {
        // 生成唯一命令ID(确保幂等性)
        this.commandId = "TRANSFER_" + UUID.randomUUID().toString().substring(0, 8);
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.amount = amount;
        this.fundService = fundService;

        // 初始化操作日志
        this.log = new OperationLog();
        this.log.setCommandId(commandId);
        this.log.setOperator(operator);
        this.log.setOperateTime(LocalDateTime.now());
        this.log.setType("FUND_TRANSFER");
        this.log.setParams(String.format("从账户%s到%s,金额%s元", fromAccount, toAccount, amount));
    }

    @Override
    public boolean execute() {
        try {
            // 调用接收者执行实际业务逻辑
            boolean success = fundService.transferWithIdempotency(
                commandId, fromAccount, toAccount, amount);

            // 更新日志结果
            log.setResult(success ? "SUCCESS" : "FAIL:划拨失败");
            return success;
        } catch (Exception e) {
            log.setResult("FAIL:" + e.getMessage());
            return false;
        } finally {
            // 保存日志(实际中写入数据库)
            LogRepository.save(log);
        }
    }

    @Override
    public String getCommandId() {
        return commandId;
    }

    @Override
    public OperationLog getOperationLog() {
        return log;
    }
}
2.4 调用者与日志存储
import java.util.ArrayList;
import java.util.List;

/**
 * 命令审计器(调用者)
 */
public class CommandAuditor {
    // 命令执行历史
    private final List<AuditableCommand> commandHistory = new ArrayList<>();

    /**
     * 执行命令并记录日志
     */
    public boolean executeCommand(AuditableCommand command) {
        System.out.println("执行命令:" + command.getCommandId() + ",操作人:" + command.getOperationLog().getOperator());
        boolean success = command.execute();
        commandHistory.add(command);
        System.out.println("命令" + command.getCommandId() + "执行完成,结果:" + command.getOperationLog().getResult());
        return success;
    }

    /**
     * 重试失败的命令
     */
    public boolean retryFailedCommand(String commandId) {
        for (AuditableCommand command : commandHistory) {
            if (command.getCommandId().equals(commandId) && 
                command.getOperationLog().getResult().startsWith("FAIL")) {
                System.out.println("重试命令:" + commandId);
                return command.execute();
            }
        }
        throw new IllegalArgumentException("未找到命令或命令无需重试:" + commandId);
    }
}

/**
 * 日志存储(模拟)
 */
public class LogRepository {
    public static void save(OperationLog log) {
        // 实际中会写入数据库或日志系统
        System.out.println("保存日志:" + log.getCommandId() + ",结果:" + log.getResult());
    }
}
2.5 客户端使用示例
import java.math.BigDecimal;

/**
 * 支付服务客户端
 */
public class PaymentService {
    public static void main(String[] args) {
        // 1. 创建接收者
        FundService fundService = new FundService();
        System.out.println("初始余额:ACC001=" + fundService.getBalance("ACC001") + 
                ",ACC002=" + fundService.getBalance("ACC002"));

        // 2. 创建命令
        AuditableCommand transferCommand = new FundTransferCommand(
            "ACC001", "ACC002", new BigDecimal("2000"), 
            fundService, "admin");

        // 3. 创建调用者
        CommandAuditor auditor = new CommandAuditor();

        // 4. 执行命令
        System.out.println("\n=== 第一次执行命令 ===");
        auditor.executeCommand(transferCommand);

        // 5. 查看执行后余额
        System.out.println("\n执行后余额:ACC001=" + fundService.getBalance("ACC001") + 
                ",ACC002=" + fundService.getBalance("ACC002"));

        // 6. 尝试重复执行(验证幂等性)
        System.out.println("\n=== 重复执行命令(验证幂等性) ===");
        auditor.executeCommand(transferCommand);

        // 7. 查看最终余额(应与上次相同,无重复划拨)
        System.out.println("\n最终余额:ACC001=" + fundService.getBalance("ACC001") + 
                ",ACC002=" + fundService.getBalance("ACC002"));
    }
}

3. 模式价值体现

  • 操作与审计解耦:资金划拨逻辑(FundService)与日志审计逻辑(CommandAuditor)完全分离,各自专注于单一职责
  • 幂等性保障:通过唯一commandId确保命令不会被重复执行,解决支付系统中关键操作的重复执行问题
  • 可追溯性:每个命令的执行过程被完整记录在OperationLog中,支持审计追踪和问题排查
  • 支持重试:调用者可通过retryFailedCommand方法重试失败的命令,无需重新构建参数
  • 扩展性强:新增支付操作(如退款、冻结)只需实现AuditableCommand接口,无需修改调用者和日志存储逻辑

五、开源框架中命令模式的运用

Netty的ChannelHandler机制为例,说明命令模式在开源框架中的典型应用:

1. 核心实现分析

Netty是一个高性能的网络通信框架,其ChannelHandler机制基于命令模式,将网络事件(如连接、读取、写入)封装为命令,由ChannelPipeline(调用者)负责触发事件处理,ChannelHandler(具体命令)负责处理事件。

1.1 命令接口与事件

Netty的ChannelHandler接口扮演命令接口的角色,定义了处理网络事件的方法:

public interface ChannelHandler {
    // 处理通道激活事件(连接建立)
    void channelActive(ChannelHandlerContext ctx) throws Exception;

    // 处理读取事件(接收数据)
    void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;

    // 处理异常事件
    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}
1.2 具体命令实现

ChannelInboundHandlerAdapterChannelHandler的默认实现,用户可通过继承它实现自定义事件处理(具体命令):

public class CustomHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 处理读取事件(如解析支付协议、处理请求)
        System.out.println("收到数据:" + msg);
        // 转发给下一个处理器
        ctx.fireChannelRead(msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 处理异常(如网络错误)
        cause.printStackTrace();
        ctx.close();
    }
}
1.3 调用者与执行流程

ChannelPipeline作为调用者,持有ChannelHandler链,负责将网络事件分发到对应的处理器:

// 初始化Netty服务端时配置处理器
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) {
            ChannelPipeline pipeline = ch.pipeline();
            // 添加具体命令(处理器)
            pipeline.addLast(new CustomHandler());
            pipeline.addLast(new PaymentProtocolHandler());
            pipeline.addLast(new BusinessHandler());
        }
    });

当网络事件发生时,ChannelPipeline会依次调用链中的ChannelHandler处理事件,实现事件的链式处理。

2. 命令模式在Netty中的价值

  • 事件处理解耦:网络事件的产生者(如NioSocketChannel)与处理者(ChannelHandler)完全解耦,前者无需知道事件如何处理
  • 灵活扩展:用户可通过添加自定义ChannelHandler扩展功能(如加密、协议解析),无需修改框架核心
  • 事件链式处理:多个ChannelHandler可组成处理链,实现复杂事件的分步处理(如支付请求的解析→验证→处理)
  • 可插拔性ChannelHandler可动态添加或移除,支持运行时调整事件处理逻辑

六、总结

1. 命令模式的适用场景

  • 当需要将请求发送者与接收者解耦,使两者不直接交互时
  • 当需要支持请求的排队、日志记录、撤销或重试时
  • 当系统中存在多种请求类型,且需要对这些请求进行统一管理时
  • 当需要实现命令的组合执行(如批量操作)时
  • 当需要在不同时间点执行请求(如定时任务、异步处理)时

2. 命令模式与其他模式的区别

  • 与策略模式:两者都通过接口实现多态,但策略模式专注于算法的封装与切换,命令模式专注于请求的封装与解耦,前者是“做什么算法”,后者是“做什么操作”
  • 与观察者模式:观察者模式是一对多的通知机制,命令模式是请求的封装与执行,前者强调状态变化的通知,后者强调请求的处理
  • 与备忘录模式:两者都支持撤销操作,但备忘录模式通过保存对象状态实现撤销,命令模式通过命令的undo方法实现撤销,前者更关注状态恢复,后者更关注操作回滚

3. 支付系统中的实践价值

  • 安全性保障:通过幂等性设计和日志记录,确保支付系统中关键操作的安全性和一致性
  • 可维护性提升:请求与处理逻辑解耦,每个命令专注于单一操作,便于代码维护和测试
  • 业务扩展性增强:新增支付操作只需添加命令实现,无需修改原有核心逻辑
  • 审计合规支持:完整的命令日志满足金融系统的审计和合规要求
  • 故障恢复能力:支持命令重试和撤销,提升支付系统的故障恢复能力

4. 实践建议

  • 确保命令幂等性:支付系统中的命令必须实现幂等性,避免重复执行导致资金风险
  • 合理设计命令粒度:命令粒度不宜过粗(难以复用)或过细(增加管理成本),以单一业务操作为宜
  • 日志完整记录:命令的执行日志应包含足够信息(参数、结果、时间、操作人),支持全链路追溯
  • 异步处理命令:非实时操作(如对账、通知)建议通过异步队列执行命令,提高系统吞吐量
  • 警惕过度设计:简单场景(如无日志、重试需求)无需使用命令模式,避免增加系统复杂度

命令模式通过将请求封装为对象,为支付系统中的操作管理、日志审计和故障恢复提供了灵活的解决方案。它不仅是一种设计模式,更是一种“请求管理”的思想,合理应用可显著提升支付系统的安全性、可维护性和扩展性。