
走进 Gang of Four 设计模式命令模式说明不仅告诉你怎么用更告诉你为什么这样设计前置知识Java 面向对象基础接口、多态、回调思想、Runnable 基础 设计模式分类总览GoF 23 种设计模式可按两个维度交叉分类类/对象处理方式 ×创建型/结构型/行为型目的。维度创建型Creational结构型Structural行为型Behavioral类Class通过继承复用Factory Method工厂方法Adapter类适配器Interpreter解释器Template Method模板方法对象Object通过组合/聚合复用Abstract Factory抽象工厂Builder建造者Prototype原型Singleton单例Adapter对象适配器Bridge桥接Composite组合Decorator装饰Facade外观Flyweight享元Proxy代理Chain of Resp.责任链Command命令Iterator迭代器Mediator中介者Memento备忘录Observer观察者State状态Strategy策略Visitor访问者 目录模式概述 · 15 问深度分析框架源码实战分析JDK Runnable 与 ThreadPoolExecutorSpring JdbcTemplate StatementCallbackTomcat SocketProcessor深度追问Why / How / Trade-off / Evolution / Modern Practice总结参考文献1. 模式概述 · 15 问深度分析Q1: 为什么需要这个模式它解决了什么问题在软件开发中我们经常需要向某些对象发送请求例如执行、撤销、排队、记录日志。核心问题传统的直接调用会导致行为发送者与行为接收者强耦合。解决的问题命令模式解决了行为发送者与行为接收者之间的紧耦合问题。它实现了将请求转换为一个对象从而使你可用不同的请求对客户进行参数化。它还顺带解决了请求的排队、记录日志、可撤销/重做Undo/Redo等事务性需求。Q2: 如果不用这个模式会有什么缺陷如果不使用命令模式发送者必须直接持有接收者的引用并调用其方法// ❌ 发送者直接调用执行者紧耦合button.onClick(()-orderService.buy(ABC,100));// 无法实现撤销、排队、记录日志等功能硬编码与强耦合遥控器必须知道电视机的turnOn()方法。如果想控制电风扇必须修改遥控器代码。难以扩展复杂行为无法优雅实现撤销Undo和恢复Redo。无法异步或延迟执行请求必须立即执行无法放入队列排队。Q3: 核心思想是什么一句话如何概括核心思想把方法调用拉高一个维度抽象并包装成一个独立的内存对象把动作变成物品。一句话概括将请求封装为对象实现发送者与接收者的彻底解耦。Q4: 包含哪些角色每个角色的职责是什么角色职责Command抽象命令声明执行操作的接口通常有execute()有时包含undo()ConcreteCommand具体命令持有 Receiver 的引用在execute()中调用 Receiver 的方法Invoker调用者持有命令对象触发execute()不直接接触 ReceiverReceiver接收者真正执行业务逻辑的对象Client客户端创建具体命令并绑定 Receiver设置到 InvokerQ5: 它们之间如何协作调用流程是怎样的协作流程以遥控器开灯为例组装期Client 负责客户端创建LightReceiver和LightOnCommandConcreteCommand将Light注入到LightOnCommand中再将命令设置到RemoteControlInvoker中。触发期Invoker 负责用户按下遥控器按钮RemoteControl调用command.execute()。执行期Command 内部转化LightOnCommand的execute()触发内部调用light.on()。Q6: 为什么要这样设计每个角色存在的意义是什么Receiver 的意义专心做纯粹的底层业务逻辑不需要关心是谁、在什么场景下调用的它。Invoker 的意义作为统一的触发器如 UI 按钮只需要知道有个命令可以执行不关心具体是什么命令。ConcreteCommand 的意义作为粘合剂把具体的 Receiver 和具体的动作绑定在一起转换成一个可传递、可存储的对象卡片。Q7: 为什么使用接口、抽象类、组合而不是其他方式为什么用接口/抽象类Command为了利用多态。Invoker 只需要面向Command接口编程就可以在运行时动态替换不同的具体命令。为什么用组合ConcreteCommand 持有 Receiver通过组合而非继承具体命令可以在运行时动态绑定不同的接收者。Q8: 这种设计符合哪些面向对象原则单一职责原则 (SRP)触发请求的类Invoker和执行请求的类Receiver各司其职。开闭原则 (OCP)引入新的具体命令时无需修改现有的 Invoker 或 Receiver 代码。依赖倒置原则 (DIP)Invoker 和 ConcreteCommand 都依赖于抽象的Command接口。Q9: 它有哪些优点和局限性优点发送者与接收者完全解耦。极易扩展新命令符合 OCP。方便实现命令队列、日志记录、Undo/Redo 功能。局限性类数量爆炸每一个细小的动作都需要一个独立的ConcreteCommand类。Q10: 最适合/不该用的场景最适合需要支持 Undo/Redo撤销/重做的场景如富文本编辑器、画图软件。需要将请求排队执行、记录日志或延迟执行的场景如线程池任务队列。界面通用组件与后台逻辑解耦如 GUI 里的菜单项、快捷键映射。不应该使用系统的业务非常简单发送者和接收者属于同一个模块且不需要撤销或队列功能。Q11: 与其他模式的区别与策略模式Strategy的区别策略模式关注的是如何做算法的替换命令模式关注的是做什么将请求封装。与备忘录模式Memento的联系实现高级 Undo 时命令模式常与备忘录模式组合使用。命令记录做了什么动作备忘录保存和恢复动作前后的状态。Q12: 框架中有哪些典型应用Java / JDKjava.lang.Runnable接口。Runnable CommandThreadPoolExecutor Invoker。Spring 框架JdbcTemplate中的StatementCallback。Spring 管理连接和关闭InvokerSQL 执行封装在 Callback 对象Command中。Tomcat将请求封装为Runnable任务放入线程池中异步执行。Q13: 如何从零实现一个最小可运行版本// 1. 抽象命令interfaceCommand{voidexecute();}// 2. 接收者classLight{publicvoidon(){System.out.println(灯亮了);}}// 3. 具体命令classLightOnCommandimplementsCommand{privateLightlight;publicLightOnCommand(Lightlight){this.lightlight;}Overridepublicvoidexecute(){light.on();}}// 4. 调用者classSimpleRemoteControl{privateCommandslot;publicvoidsetCommand(Commandcommand){this.slotcommand;}publicvoidbuttonWasPressed(){slot.execute();}}// 5. 客户端publicclassMain{publicstaticvoidmain(String[]args){SimpleRemoteControlremotenewSimpleRemoteControl();LightlightnewLight();CommandlightOnnewLightOnCommand(light);remote.setCommand(lightOn);remote.buttonWasPressed();// 灯亮了}}Q14: 识别重构信号巨型switch-case或if-else通过判断不同请求类型来调用不同对象的方法。硬编码的请求触发UI 层直接调用 Service 层的方法导致 UI 无法独立复用。有回滚或重试的需求代码中充满手工保存旧状态并试图恢复的手工逻辑。Q15: 现代改进A. 函数式编程Lambda消灭类爆炸// 不需要写 LightOnCommand 类了remote.setCommand(light::on);remote.buttonWasPressed();B. CQRS命令查询职责分离写操作被封装为统一的CommandDTO发送到CommandBusInvoker路由到对应的CommandHandlerReceiver执行。C. Saga 分布式事务模式每个微服务操作是一个Command提供CompensatingCommand补偿命令/高级版undo()。失败时协调器倒序调用补偿命令实现最终一致性。2. 框架源码实战分析2.1 JDK Runnable 与 ThreadPoolExecutor这是 Java 生态中最纯正、最基础的命令模式实现。角色映射角色JDK 对应CommandRunnable接口ConcreteCommand实现了Runnable的类或 LambdaInvokerThreadPoolExecutorReceiver具体命令内部调用的业务对象抽象命令CommandFunctionalInterfacepublicinterfaceRunnable{publicabstractvoidrun();}调用者Invoker——ThreadPoolExecutor.execute()publicvoidexecute(Runnablecommand){if(commandnull)thrownewNullPointerException();intcctl.get();// 1. 工作线程少于核心线程数直接创建新线程执行if(workerCountOf(c)corePoolSize){if(addWorker(command,true))return;cctl.get();}// 2. 核心线程满了把命令放入阻塞队列排队——命令模式的经典优势if(isRunning(c)workQueue.offer(command)){intrecheckctl.get();if(!isRunning(recheck)remove(command))reject(command);elseif(workerCountOf(recheck)0)addWorker(null,false);}// 3. 队列满了尝试开辟非核心线程elseif(!addWorker(command,false))reject(command);}深度分析ThreadPoolExecutor内部的Worker从队列取出Runnable后调用command.run()。请求的触发时机线程调度与具体执行内容run()内部逻辑实现了解耦。这就是命令模式时间解耦的经典体现——请求可以放入队列在未来的某个时刻由工作线程异步执行。2.2 Spring JdbcTemplate StatementCallback角色映射角色Spring 对应CommandStatementCallbackTConcreteCommandLambda 表达式封装 SQLInvokerJdbcTemplateReceiverStatement/Connection抽象命令FunctionalInterfacepublicinterfaceStatementCallbackT{TdoInStatement(Statementstmt)throwsSQLException,DataAccessException;}调用者JdbcTemplate.execute()publicTTexecute(StatementCallbackTaction)throwsDataAccessException{// 1. 获取数据库连接Invoker环境准备ConnectionconDataSourceUtils.getConnection(obtainDataSource());Statementstmtnull;try{stmtcon.createStatement();applyStatementSettings(stmt);// 2. 核心触发调用命令对象的 doInStatementTresultaction.doInStatement(stmt);handleWarnings(stmt);returnresult;}catch(SQLExceptionex){StringsqlgetSql(action);throwtranslateException(StatementCallback,sql,ex);}finally{// 3. 释放资源Invoker善后清理JdbcUtils.closeStatement(stmt);DataSourceUtils.releaseConnection(con,getDataSource());}}客户端使用jdbcTemplate.execute((StatementCallbackObject)stmt-{stmt.execute(DROP TABLE IF EXISTS user);returnnull;});深度分析JdbcTemplateInvoker只管开连、关连、异常处理。具体是做DROP TABLE还是INSERT全部封装在StatementCallbackCommand中。这就是命令模式结构解耦——把复杂的流程控制和易变的业务动作彻底剥离开。2.3 Tomcat SocketProcessor角色映射角色Tomcat 对应CommandRunnableSocketProcessorBaseConcreteCommandSocketProcessorInvokerAbstractEndpoint内部线程池ReceiverHttp11Processor具体命令封装publicabstractclassSocketProcessorBaseSimplementsRunnable{protectedSocketWrapperBaseSsocketWrapper;protectedSocketEventevent;Overridepublicfinalvoidrun(){doRun();}protectedabstractvoiddoRun();}调用者投递命令publicbooleanprocessSocket(SocketWrapperBaseSsocketWrapper,SocketEventevent,booleandispatch){// 1. 将接收者和事件包装成具体命令SocketProcessorBaseSsccreateSocketProcessor(socketWrapper,event);ExecutorexecutorgetExecutor();if(dispatchexecutor!null){// 2. 投递给线程池异步执行executor.execute(sc);}else{sc.run();}returntrue;}接收者执行当线程池拿到SocketProcessor并运行doRun()时调用getHandler().process(socketWrapper, event)→Http11Processor解析 HTTP → 最终调用HttpServlet.service()。深度分析Tomcat 的 I/O 接收线程只管快速打包命令并分发真正耗时的协议解析和业务处理留给业务线程池。这就是命令模式在高并发场景下的经典应用。三者对比总结框架CommandInvokerReceiver核心价值JDK ThreadRunnableThreadPoolExecutor开发者自定义业务类时间解耦请求可放入队列异步执行Spring JDBCStatementCallbackJdbcTemplateStatement/Connection结构解耦流程控制与业务动作剥离TomcatSocketProcessor线程池Http11Processor高并发吞吐I/O 线程快速分发业务线程异步处理3. 深度追问3.1 Why为什么解决哪个根本矛盾命令模式解决的根本矛盾是行为的触发时机/上下文与行为的执行细节/依赖之间的矛盾。在朴素的 OOP 中如果 A 想让 B 做一件事A 必须持有 B 的引用并在特定时刻调用 B 的方法。这带来了两个致命架构锁死空间锁死依赖紧耦合触发器必须引入执行者通用的触发器无法复用。时间锁死同步强依赖调用的发生与执行被绑定在同一个线程、同一个时刻无法支持异步、排队、重试、Undo。命令模式通过引入一个中介对象Command将网状的、强耦合的调用关系重构为星型的、以数据对象为核心的解耦关系。3.2 How怎么做通过哪些对象关系和交互机制命令模式的精妙之处在于把动作Behavior升格为一等公民Object。参数化Client 把 Command 对象像参数一样塞给 Invoker。职责分离Invoker 对业务一无所知Receiver 甚至不知道自己被包装成了命令。多态路由Invoker 调用的是Command.execute()具体是A.doSomething()还是B.doAnotherThing()由多态和组合在运行时决定。3.3 Trade-off代价牺牲了什么换取什么牺牲换取类数量爆炸每个动作都需要一个 Command 类极致的扩展性OCP新增命令只需加类间接性增加代码可读性下降高级控制能力时间可控队列、状态可逆Undo、原子事务MacroCommand3.4 Evolution演化如何逐步演化而来起源C 语言的函数指针通用按钮执行不同函数只能传函数指针但缺乏状态管理。标准 GoF 命令模式把函数指针升级为持有接收者状态的对象。宏命令Macro Command变体组合模式 命令模式一键执行多个命令。撤销与重做Undo/Redo Stack变体命令模式 备忘录模式维护StackCommand。3.5 Modern Practice现代实践是否被取代在单体开发中——被 Lambda 大幅瘦身// 不再需要写 LightOnCommand 类直接用方法引用button.setAction(light::turnOn);在系统架构中——演进为 CQRS写操作封装为统一的 Command DTO发送到 CommandBusInvoker路由到对应的 CommandHandlerReceiver成为 DDD 和高并发架构的核心基石。在分布式事务中——演进为 Saga 模式每个微服务操作是一个 Command提供 CompensatingCommand补偿命令。失败时协调器倒序调用补偿命令实现最终一致性。总结你可能很少再手写XxxCommand类了因为 Lambda 在语言层面终结了它的臃肿但在微服务、DDD 和事件驱动架构中把请求封装成对象实现异步、解耦和可补偿控制的核心哲学依然是现代架构师最锋利的武器之一。4. 总结核心要点命令模式将请求封装为对象实现发送者和执行者的解耦命令对象可以排队、记录、撤销、组合——这是它最大的灵活性Runnable是 JDK 中最简单的命令模式实例三个工业级变体JDK ThreadPool时间解耦、Spring JdbcTemplate结构解耦、Tomcat SocketProcessor高并发吞吐现代演进Lambda 消除类爆炸、CQRS 架构、Saga 分布式事务一句话命令模式把做什么打包成一个对象——就像餐厅里服务员只负责传菜单命令厨师接收者负责做菜。5. 参考文献[1] GAMMA E, HELM R, JOHNSON R, et al. Design Patterns: Elements of Reusable Object-Oriented Software[M]. Boston: Addison-Wesley, 1994.[2] Oracle. Java Runnable / ThreadPoolExecutor Source Code[EB/OL]. https://docs.oracle.com/javase/8/docs/api/, 2024.[3] SPRING. Spring JdbcTemplate Documentation[EB/OL]. https://docs.spring.io/spring-framework/reference/data-access/jdbc.html, 2024.[4] Apache Tomcat. Tomcat Source Code[EB/OL]. https://tomcat.apache.org/, 2024.