QT C++实战:一套工业软件架构的5个核心设计思想
从”能用”到”好用”:一套工业软件架构的5个核心设计思想
设备跑起来的时候没人关心你用了什么设计模式,只在乎它会不会停。
🎯 开篇:工业软件的架构之痛
做工业软件的朋友都知道,设备跑起来的时候没人关心你用了什么设计模式,只在乎它会不会停。
但作为一个有追求的架构师,我们既要让设备7×24小时稳定运行,也要让代码优雅得像一首诗。
今天我想跟大家分享的,是HY-HMI这套架构背后的5个核心设计思想——不是教科书上的理论,而是我们在半导体封装设备上踩坑踩出来的真知灼见。
🏗️ 一、设计思想一:微内核,大生态
1.1 为什么是微内核,不是微服务
很多人一提到插件化就想到微服务,但工业软件绝对不能搞微服务那一套!
工业软件 vs 互联网软件的本质区别

我的核心设计思想:进程内微内核插件化,而不是跨进程微服务化。
所有的模块解耦都在同一个进程内完成,所有插件之间的调用都是直接的函数调用,没有序列化反序列化开销。
这就是为什么我们的RPC调用平均延迟只有0.12ms的根本原因。
1.2 微内核的三大核心原则
// 微内核三大原则:
//
// 1. 核心稳定不变 —— 服务总线、日志、权限
// 2. 业务插件可变 —— 运动控制、视觉、工序
// 3. 依赖方向唯一 —— 只能内核,绝对不能反过来
大家看我们代码结构就知道这个设计思想的贯彻有多彻底:
-
src/core/ 目录下的10个核心服务文件,半年来几乎没变过
-
src/plugins/ 下面30多个插件,改了无数轮
-
没有任何一个核心服务依赖任何一个插件
-
所有的插件都只依赖核心服务的接口定义
这就是架构的力量——不变的内核,万变的插件。
🚄 二、设计思想二:总线即操作系统
如果说微内核是人的心脏,那么服务总线就是连接全身的血管。
2.1 服务总线不是消息队列,是操作系统内核
很多人看ServiceBus代码,说这不就是个消息转发吗?
大错特错!
服务总线本质上,就是我们在进程内实现了一个极简版的 “操作系统”。
/*** 服务总线设计思想:** 1. 服务注册表 ≈ 进程表* 每个注册的服务就是一个"进程"** 2. 消息发布订阅 ≈ IPC进程间通信* 插件之间通过总线发消息就是进程间发消息** 3. RPC调用 ≈ 系统调用* 跨插件方法调用就是执行系统调用** 4. 健康检查 ≈ 进程监控* 挂了的服务自动重启** 5. 熔断器 ≈ 内存保护* 一个进程崩了不影响其他*/class ServiceBus {// 这不是设计模式,这是操作系统原理};
2.2 熔断器设计的哲学
很多人做熔断器只做了 “熔断”,没做 “自愈”。
我的设计思想是:熔断器不是用来”防雪崩”的,是用来”给次机会”的。
// 三种状态的人生哲学://// Closed → 相信你,让你过// ↓出问题了// Open → 你先歇会儿// ↓冷静一下// HalfOpen → 再给你一次机会
现场设备最怕什么?不是出问题,是出了问题没人管。熔断器给了出错的模块一次 “重新做人” 的机会,也给了运维人员 “发现问题” 的时间。
工业软件,容错就是生产力。
2.3 RPC调用的超时与异步处理
RpcResponse ServiceBus::rpc(const QString& serviceName,const QString& method,const QVariantList& args,int timeoutMs){// Step 1: 检查熔断器if (!checkCircuitBreaker(serviceName)) {return RpcResponse{generateRequestId(),false,QVariant(),"Circuit breaker is open",0};}QElapsedTimer timer;timer.start();// Step 2: 获取服务实例IService* service = getService(serviceName);if (!service) {recordFailure(serviceName);return RpcResponse{generateRequestId(),false, QVariant(),QString("Service %1 not found").arg(serviceName),timer.elapsed()};}// Step 3: 执行调用(带超时检测)QVariant result = service->call(method, args);// Step 4: 更新熔断器状态if (result.isValid()) {recordSuccess(serviceName);} else {recordFailure(serviceName);}updateStatistics(result.isValid(), timer.elapsed());return RpcResponse{generateRequestId(),result.isValid(),result,result.isValid() ? "" : "Method call failed",timer.elapsed()};}
📝 三、设计思想三:日志即数据库
在工业现场,“出了问题能找到原因”比”不出问题”更重要。
3.1 结构化日志的真正价值
// 分级日志定义:Trace/Debug/Info/Warning/Error/Critical/Fatalenum classLogLevel{Trace = 0, // 跟踪级别:详细的程序执行流程Debug = 1, // 调试级别:调试信息Info = 2, // 信息级别:正常运行信息Warning = 3, // 警告级别:潜在问题Error = 4, // 错误级别:错误但可恢复Critical = 5, // 严重错误Fatal = 6 // 致命错误};// 多目标同时输出:Console/File/Database/Remoteenum classLogTarget{Console = 0x01, // 控制台输出(带颜色区分)File = 0x02, // 文件输出(自动轮转)Database = 0x04, // 数据库存储(批量写入)Remote = 0x08 // 远程上报(工厂级日志平台)};
现场出问题的时候:
-
我要 “运动控制模块昨天下午3点到5点所有的Error日志”
-
我要 “某个ID为1234的产品” 所有相关日志
-
我要 “某个线程” 所有的日志
-
我要 “某台设备” 所有Fatal日志
如果你的日志就是一串文本,你查去吧!
3.2 性能设计的哲学
工业软件日志,日志系统卡了业务逻辑,那就是犯罪。
我的性能设计三原则:
-
调用方绝对不阻塞 —— 进队列就返回
-
IO线程做真正的写操作
-
队列满了丢老日志,绝对不OOM
// 这行代码背后是血的教训:while (m_logBuffer.size() > MAX_BUFFER_SIZE) {m_logBuffer.dequeue(); // 丢最老的,保住最新的}
设备死机了,最新的日志才值钱。
🔌 四、设计思想四:面向契约,依赖抽象
插件化的精髓是什么?不是用了Qt的Plugin框架,那只是技术手段。
插件化的精髓,是面向接口编程,是依赖倒置原则。
4.1 接口设计的艺术
// IPlugin接口设计思想解读:virtual QList<Dependency> dependencies() const = 0;// ↑ 你需要什么,清清楚楚写明白virtualboolinitialize(const QVariantMap& config) = 0;// ↑ 给你什么配置virtual QObject* getService(const QString& serviceName) = 0;// ↑ 你提供什么服务virtualboolhandleMessage(const QString& messageType, const QVariant& data) = 0;// ↑ 你处理什么消息
就是这么4个方法,定义了插件与外界所有的边界。
一个插件,你从加载进来,我就知道你是谁,你需要什么,你能做什么。
4.2 依赖管理的哲学
做插件化最容易踩的坑是什么?循环依赖!
我的设计思想:依赖就是承诺,承诺就要负责到底。
// Kahn算法拓扑排序的意义是什么?// 不是算法本身,而是检测循环依赖。//// A → B → C → A// 只要发现循环依赖,直接加载失败//// 编译期放过你,加载期绝对弄死你。
工程师写代码的时候图方便写了个循环依赖,架构师必须在架构层面给你把关。
技术债务不能妥协,就是对质量犯罪。
4.3 热插拔设计的哲学
热插拔不是给客户装X用的!
热插拔是:
-
现场紧急加个统计报表,不用停设备
-
某个小功能有Bug,修复了不影响生产
-
客户现场调试,加几行代码看个变量
在半导体工厂,设备停一小时,损失是以万为单位计算的。
🎨 五、设计思想五:分离关注点
工业软件UI层90%的烂代码,都源于一个问题:
在按钮的clicked()槽函数里写了800行运动控制的代码。
5.1 View干View的事,ViewModel干ViewModel的事,Service干Service的事
谁也别越界!
为什么要分这么清楚?
-
刚毕业的小姑娘写QML画界面,她不用懂运动控制
-
十年经验的老法师写运动控制算法,他不用懂QML
-
测试工程师直接测ViewModel,不用跑UI自动化
分工,就是生产力。
5.2 ViewModel基类设计
class BaseViewModel : public QObject{Q_OBJECTQ_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged)Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorChanged)Q_PROPERTY(bool hasError READ hasError NOTIFY errorChanged)public:virtual void initialize() = 0;virtualvoidcleanup() = 0;protected:voidsetLoading(bool loading);voidsetError(const QString& error);IService* getService(const QString& serviceName);};
5.3 运动控制ViewModel示例
class MotionControlViewModel : public BaseViewModel{Q_OBJECTQ_PROPERTY(QVariantList axes READ axes NOTIFY axesChanged)Q_PROPERTY(double currentPosition READ currentPosition NOTIFY positionChanged)Q_PROPERTY(bool isMoving READ isMoving NOTIFY movingChanged)public:// 暴露给QML调用的方法Q_INVOKABLE void moveAbsolute(double position);Q_INVOKABLE voidmoveRelative(double distance);Q_INVOKABLE voidstop();Q_INVOKABLE voidhome();private slots:// 响应服务层的状态变化voidonAxisPositionChanged(int axisId, double position);voidonAxisStateChanged(int axisId, int state);};
这样做的好处是什么?
-
View层的工程师不用懂运动控制,只需要绑定数据即可
-
业务逻辑的工程师写ViewModel,不用关心界面怎么画
-
单元测试可以直接测ViewModel,不需要UI自动化
5.4 数据绑定的性能优化
很多人做数据绑定,变量一变就notify,然后UI卡成狗。
我的设计思想:变化小于编码器精度的,就别刷新了。
// 电机1毫秒发一次位置,UI60帧刷新就够了。// UI刷那么快,人眼也看不到。voidonAxisPositionChanged(int axisId, double position) {// 位置变化小于0.1微米if (qAbs(m_currentPosition - position) > 0.0001) {m_currentPosition = position;emit positionChanged();}}
📊 六、设计思想落地效果验证
讲了这么多设计思想,到底有用吗?有用。
|
设计思想 |
带来的结果 |
|---|---|
|
微内核 |
核心半年不变,插件随便改 |
|
熔断器 |
单模块挂了设备照样跑 |
|
结构化日志 |
出问题5分钟定位 |
|
接口契约 |
并行开发不冲突 |
|
MVVM分层 |
新人两周上手 |
最终体现在:
-
新功能开发速度提升4倍
-
线上Bug率下降7倍
-
设备MTBF从72小时 → 500+小时
🎯 总结:架构设计的本质
文章的最后,我想跟所有做工业软件同行说句掏心窝子的话。
我们做工业软件架构设计,不是为了炫技,不是为了面试能吹牛逼。
我们做的每一个设计决策,背后都是:
-
现场工程师少熬一个夜
-
客户少损失几十万
-
操作员少担一点心
我总结这一套架构设计思想,最后都浓缩成三句话:
-
微内核大生态 —— 不变的内核,万变的插件
-
容错就是生产力 —— 出问题不可怕,可怕的是出了问题没人管
-
分工就是竞争力 —— 让专业的人做专业的事
我们写的每一行代码,最后都会跑到工厂里,跑到设备上,跑在客户的生产线上。对代码负责,就是对客户负责,就是对自己负责。

夜雨聆风