需求递进是软件开发的第一定律。你写了一个按钮点击保存文件。一开始它只是保存后来要检查文件是否被占用再后来要记录操作日志再再后来要校验权限。这还没完产品经理说操作慢的时候能不能打个性能日志。一个简单的保存按钮从 5 行代码膨胀到 50 行。更可怕的是每个按钮都要来一遍。Command 模式把操作封装成对象Qt 原生就有QAction但它的设计偏菜单工具栏不适合复杂交互场景。Aether 里自己实现了一套 Command 基类classCommand:publicQObject{Q_OBJECTQ_PROPERTY(boolcanExecute READ canExecute NOTIFY canExecuteChanged)public:virtualvoidexecute()0;virtualboolcanExecute()const{returnm_canExecute;}voidsetCanExecute(boolcan);voidsetName(constQStringname);QStringname()const;voidsetViewModel(ViewModel*vm);ViewModel*viewModel()const;// 中间件管道voidaddMiddleware(CommandMiddlewarePtr middleware);voidclearMiddleware();publicslots:voidrun();// 经中间件管道执行命令signals:voidcanExecuteChanged(boolcanExecute);};核心就三个东西execute()执行、canExecute()控制可用性、run()走中间件管道后执行。FunctionalCommand是它的 lambda 版本大多数场景都用这个classFunctionalCommand:publicCommand{public:usingExecuteFunctionstd::functionvoid();usingCanExecuteFunctionstd::functionbool();explicitFunctionalCommand(ExecuteFunction executeFunc,CanExecuteFunction canExecuteFuncnullptr,QObject*parentnullptr);voidexecute()override;boolcanExecute()constoverride;voidupdateCanExecute();};用法很简单autocmdnewFunctionalCommand([this](){saveFile();},[this](){returnm_isFileDirty;},this);cmd-setName(saveFile);cmd-setViewModel(this);// 绑到按钮DataBinding::bindCommand(cmd,ui-btnSave);到此为止就是标准的 Qt Command 模式没什么新鲜的。真正让这套东西脱胎换骨的是下面的中间件管道。一个按钮的熵增过程先看一段真实的业务代码。假设你在处理产品管理中的添加产品操作// Bad: 所有逻辑揉在一个函数里voidProductViewModel::addProduct(){// ── 权限检查 ──if(!PermService::instance()-can(addProduct)){ui-statusLabel-setText(权限不足);return;}// ── 数据校验 ──if(m_nameEdit-text().isEmpty()||m_priceSpin-value()0){ui-statusLabel-setText(请输入完整的产品信息);return;}// ── 业务逻辑 ──Product p;p.namem_nameEdit-text();p.pricem_priceSpin-value();ProductRepository::addProduct(p);// ── 日志 ──Logger::log(添加产品: p.name);// ── 性能记录 ──PerformanceTracker::record(addProduct);}这段代码有什么问题每个职责都写在同一个函数里。权限校验、数据校验、业务逻辑、日志、性能监控全搅在一起。等下一个按钮删除产品写出来你会不自觉地复制粘贴这坨代码然后改两行参数。五个按钮之后项目里就多了五份相似但不同、改一个漏三个的屎山副本。中间件管道洋葱模型Aether 的解法是——中间件管道。它很像你熟悉的 HTTP 中间件每个中间件只关心一件事通过next()串联起来。管道核心就是一个递归分发MW[0].pre → MW[1].pre → terminal(实际逻辑) → MW[1].post → MW[0].post来看 Aether 的CommandMiddlewarePipelineclassCommandMiddlewarePipeline{public:usingTerminalstd::functionvoid(CommandContext);voidadd(CommandMiddlewarePtr middleware);voidclear();boolisEmpty()const;intcount()const;voidrun(CommandContextctx,Terminal terminalnullptr)const;private:voiddispatchAt(std::size_t index,CommandContextctx,constTerminalterminal)const;std::vectorCommandMiddlewarePtrm_middlewares;};递归分发的实现只有十几行voidCommandMiddlewarePipeline::dispatchAt(std::size_t index,CommandContextctx,constTerminalterminal)const{if(indexm_middlewares.size()){if(terminal)terminal(ctx);return;}constautomwm_middlewares[index];mw-process(ctx,[this,index,terminal](CommandContextc){dispatchAt(index1,c,terminal);});}每个中间件实现ICommandMiddleware接口classICommandMiddleware{public:usingNextstd::functionvoid(CommandContext);virtualvoidprocess(CommandContextctx,Next next)0;};上下文对象传递了命令名称、关联的 ViewModel 和一个 cancelled 标志structCommandContext{QString commandName;ViewModel*viewModel;boolcancelled;explicitCommandContext(constQStringname,ViewModel*vmnullptr):commandName(name),viewModel(vm),cancelled(false){}};任何一个中间件把ctx.cancelled设为 true 并跳过next()整个链条就终止了。四组内置中间件Aether 提供了四组开箱即用的中间件覆盖了 90% 的横切关注点。1. 日志中间件 (LoggingCommandMiddleware)classLoggingCommandMiddleware:publicICommandMiddleware{public:voidprocess(CommandContextctx,Next next)override{log(→ 进入: ctx.commandName);next(ctx);if(ctx.cancelled)log(← 取消: ctx.commandName ✗);elselog(← 完成: ctx.commandName ✓);}};2. 权限中间件 (AuthCommandMiddleware)classAuthCommandMiddleware:publicICommandMiddleware{public:voidsetPermitted(boolpermitted){m_permittedpermitted;}voidprocess(CommandContextctx,Next next)override{if(!m_permitted){ctx.cancelledtrue;if(m_onDenied)m_onDenied(ctx.commandName);return;}next(ctx);}};3. 数据校验中间件 (ValidationMiddleware, 属性级别)属性变更也有对应的PropertyMiddlewarePipelinestructPropertyContext{QString propertyName;QVariant oldValue;QVariant newValue;// 中间件可修改ViewModel*viewModel;boolcancelled;};校验中间件可以拦截非法输入classValidationMiddleware:publicIPropertyMiddleware{voidprocess(PropertyContextctx,Next next)override{if(ctx.propertyNameprice){doublepricectx.newValue.toDouble();if(price0){ctx.cancelledtrue;// 拒绝负价格return;}}next(ctx);}};4. 性能监控中间件 (TimingCommandMiddleware)classTimingCommandMiddleware:publicICommandMiddleware{voidprocess(CommandContextctx,Next next)override{QElapsedTimer timer;timer.start();next(ctx);qDebug()ctx.commandName耗时:timer.elapsed()ms;}};组装起来干净的代码有了这些中间件ViewModel 里的业务代码就只剩下业务逻辑了// Good: 纯业务不含横切关注点voidProductViewModel::addProduct(){Product p;p.namename();p.priceprice();p.stockstock();if(ProductRepository::addProduct(p)){setStatus(产品 p.name 添加成功);clearForm();loadProducts();}}boolProductViewModel::canAdd()const{return!name().isEmpty()price()0.0stock()0;}而横切关注点日志、权限、性能在构造函数里组装到管道上ProductViewModel::ProductViewModel(QObject*parent):ViewModel(parent){// ── 属性中间件 ──m_logMWstd::make_sharedLoggingMiddleware();m_auditMWstd::make_sharedAuditLogMiddleware();addPropertyMiddleware(m_logMW);// 第 1 层日志addPropertyMiddleware(m_auditMW);// 第 2 层审计// ── 命令 ──m_addCommandnewFunctionalCommand([this](){addProduct();},[this](){returncanAdd();},this);m_addCommand-setName(addProduct);m_addCommand-setViewModel(this);// ── 命令中间件按顺序外层 → 内层 ──m_timingMWstd::make_sharedTimingCommandMiddleware();m_authMWstd::make_sharedAuthCommandMiddleware();m_cmdLogMWstd::make_sharedLoggingCommandMiddleware();m_addCommand-addMiddleware(m_timingMW);// 第 1 层计时m_addCommand-addMiddleware(m_authMW);// 第 2 层权限可取消m_addCommand-addMiddleware(m_cmdLogMW);// 第 3 层日志}执行路径变成[计时] → 开始: addProduct [AUTH] → 验证: addProduct [命令日志] → 进入: addProduct ★ execute(): addProduct ← 真正的业务只有几行 [命令日志] ← 完成: addProduct ✓ [AUTH] ← 通过: addProduct ✓ [计时] ← 结束: addProduct 耗时 3 ms ✓每一层职责单一、可复用、可开关。想关掉权限检查不 add 那个 middleware 就行。想再加一个中间件写一个类、注册一行代码不碰任何业务代码。对比一下Bad vs Good维度BadGood业务函数行数30 行揉杂所有逻辑10 行只有业务权限校验硬编码在函数开头可插拔的中间件日志记录Logger::log() 散落各处统一的 around 拦截性能监控需要手动打点 start/end自动计时包装数据校验if 判断 return可独立注册的中间件添加新横切逻辑改每个按钮函数写一个新中间件类单元测试需要 mock 日志、权限、DB只测业务中间件单独测同样的模式作用在属性上你可能会想这只对命令有用不是。相同的洋葱模型也应用在 ViewModel 的属性变更上。每个setProperty()调用都会经过PropertyMiddlewarePipelinevoidViewModel::setProperty(constQStringname,constQVariantvalue){autoitm_properties.find(name);if(itm_properties.end())return;QVariant oldValueit.value();PropertyContextctx(name,oldValue,value,this);m_propertyPipeline.run(ctx,[](PropertyContextc){if(!c.cancelled){it.value()c.newValue;// 实际写入}});}所以同样的校验、日志、审计机制既作用于命令也作用于属性变更。统一的拦截模型两个维度的应用。互动环节打开你项目里最胖的那个 ViewModel 或者 Controller。找一个按钮的点击事件数一下从入口到实际业务逻辑之间有多少行非业务代码——权限检查、日志、校验、弹窗确认。如果超过 5 行你就遇到了跟 Aether 一样的痛点。试试做个简单的CommandMiddleware接口就一个process(ctx, next)把这 5 行非业务代码抽出来。评论区告诉我你砍掉了多少行。有疑问的直接留言。如果觉得有帮助点个在看让更多人看到也欢迎转发给团队里正在重构 Qt 项目的朋友。下一篇预告——主题系统暗色模式到底难在哪里亮色/暗色切换听起来就是改几个颜色值实际上涉及到 QSS 热加载、组件渐进式适配、自定义调色板、内置控件重新绘制……这才是真正的 “换肤” 工程。这篇文章是从C到工业级Aether项目精讲系列的第 6 篇。前 5 篇分别讲了插件架构、IoC 容器和 MVVM 框架。这一篇聚焦 Command 模式与中间件管道——通过洋葱模型把横切关注点从业务代码中剥离出去。下一篇将深入主题系统看看暗色模式怎么做到真正的全局切换。