最近在写一个软件,这个软件稍微复杂一些,界面大概需要十几个,后端也是要开多线程读各种传感器数据。然后鼠鼠我呀就发现一个致命的问题,那就是前端要求的控件太多了,点一下就需要通知后端,即调用后端的函数,这是非常不友好的。因为耦合程度太高了,交互起来过于混乱严重影响开发进度。如果是一个人设计的可能稍微好一些(一个按钮实现一个函数呗),但是如果是团队分工的话就会很麻烦。后面问了大佬,大佬给我提供了一个思路,然后我会又去问了GPT。把相关的方法也写进来了。准备写个小合集,现在这里占个坑,后面在慢慢补齐。
大佬最后还给了一个建议:
要解耦,就不要想到什么功能立马实现地态度创建文件写实现,而是需要抱着写库的思路去写功能,把库(模块)写的差不多了再利用界面去调用接口
7.22日补充:后续研究发现这块内容其实应该归结于Qt的架构,所以博客被我重新修改了一些内容,同时由于开发过程中发现很多东西学习周期过于复杂,存在学习时间长,用处短的情况,所以我直接学了最后MVVM以及前后端分离的架构。
目录
1. 信号与槽(Signals & Slots)—— 小工程的"万能胶"
2. 自定义QEvent + 事件投递 —— 底层通信的"隐形桥梁"
3. 事件总线(Event Bus)—— 全局广播的"中央广播台"
4. 观察者模式(Observer Pattern)—— 设计模式的"基础积木"
6. 前后端分离(C++ + JS/Web)—— 跨平台的"终极解耦"
Qt软件架构从简单到复杂
引言
在Qt软件开发中,随着项目规模扩大、功能复杂度提升(如十几个交互界面、多线程传感器数据处理),最初的"按钮-函数"直连模式会导致前端UI与后端逻辑强耦合,团队协作效率低下。本文将系统梳理Qt开发中常用的解耦技术栈,从简单到复杂逐层解析其设计思想、技术实现、优势劣势及适用场景,助你在不同阶段选择最适合的架构方案。
一、基础解耦:Qt原生通信机制
1. 信号与槽(Signals & Slots)—— 小工程的"万能胶"
核心特点:Qt的灵魂机制,通过元对象系统(Meta-Object System)实现对象间的事件驱动通信。UI操作触发信号,后端逻辑通过槽函数响应,天然解耦UI与业务逻辑。
优势:
- 低学习成本:Qt IDE(如Qt Creator)自动生成信号槽连接代码,IDE支持可视化设计。
- 线程安全:通过
QueuedConnection
连接方式,自动处理跨线程信号传递(如后台线程通知UI更新)。 - 强类型检查:编译期检查信号与槽参数类型匹配,减少运行时错误。
劣势:
- 强依赖Qt元对象系统:仅适用于Qt项目,无法与非Qt模块(如纯C++库、Python脚本)直接通信。
- 代码膨胀风险:复杂界面(如10+按钮)会导致大量信号槽定义,维护时易出现"改一处崩一片"的情况。
- 耦合隐含性:表面解耦UI与逻辑,但信号定义仍集中在UI类中,大规模UI交互仍可能导致类膨胀。
适用场景:小型工具类应用(如本地配置工具)、Qt内部模块通信(如自定义Widget与主窗口交互)。
2. 自定义QEvent + 事件投递 —— 底层通信的"隐形桥梁"
核心特点:Qt的事件循环机制支持自定义事件类型,通过QCoreApplication::postEvent()
实现跨对象/跨线程的事件传递。适合处理系统级消息或需要精准控制的底层通信。
优势:
- 底层控制:直接利用Qt事件循环,无需额外中间层,性能接近原生函数调用。
- 跨线程友好:配合
moveToThread
实现线程间安全通信,避免直接操作线程API的复杂性。 - 类型灵活:可自定义任意数据类型的事件,适应复杂业务需求(如传感器数据、文件IO通知)。
劣势:
- 代码复杂度高:需手动管理事件类型注册、事件接收逻辑,新手易踩坑(如事件未正确处理导致内存泄漏)。
- 调试难度大:事件传递路径隐含在事件循环中,日志追踪不如信号槽直观。
- 适用范围窄:更适合处理系统级或底层事件(如硬件中断、定时任务),而非通用业务通信。
适用场景:需要与系统底层交互(如串口/网络数据接收)、高性能后台任务通知(如视频帧处理)、跨线程精确控制。
个人看法:由于事件总线在学习中的时候发现很多示例是基于信号与槽机制的(参考:2.1 用 Qt 信号-槽实现消息分发_qt receivers-CSDN博客),个人觉得虽然引进时间与槽会优化原本的系统,但是本身我不太相信信号与槽(个人观点)。而且,我没有找到有趣的开源代码故我不会再花时间在这个方向上进行研究。
二、全局解耦:事件总线与设计模式
3. 事件总线(Event Bus)—— 全局广播的"中央广播台"
核心特点:通过单例模式的消息中心,实现模块间的全局事件广播。任意模块可发布事件,订阅了该事件的模块自动接收,彻底解决多模块间强耦合问题。
优势:
- 完全解耦:发布方与订阅方无需知道彼此存在,仅需约定事件类型(如枚举MessageType),适合多团队并行开发。
- 一对多广播:一个事件可被多个模块订阅(如日志事件同时通知文件模块、网络模块、UI模块),支持插件化扩展。
- 跨模块通信:天然支持不同功能模块(如UI、网络、数据处理)间的协作,无需层层传递调用。
劣势:
- 单例管理成本:需严格保证消息中心的线程安全(如添加互斥锁),避免多线程并发发布导致的竞态条件。
- 事件类型膨胀:随着业务扩展,MessageType枚举可能变得庞大(如从10种增至100种),需规范命名与管理。
- 调试复杂性:事件传递路径隐藏在消息中心中,需依赖日志或调试工具追踪事件来源与流向。
适用场景:中大型项目的全局事件管理(如系统设置变更通知所有模块)、插件化架构(插件通过事件与主程序交互)、多模块协作(如传感器数据需同时更新UI、存储、算法)。
4. 观察者模式(Observer Pattern)—— 设计模式的"基础积木"
核心特点:经典的"发布-订阅"原型模式,主题(Subject)维护观察者(Observer)列表,状态变化时通知所有观察者。Qt虽未直接提供观察者接口,但可通过自定义接口实现。
优势:
- 松耦合经典方案:主题与观察者仅通过接口交互,符合迪米特法则(最少知识原则)。
- 语言无关性:可在非Qt项目中使用(如纯C++、Java),扩展性强。
- 灵活的订阅策略:支持动态添加/移除观察者(如运行时切换UI主题)。
劣势:
- 手动管理生命周期:需确保观察者在主题销毁前被移除,否则可能导致野指针崩溃(Qt信号槽通过元对象系统自动管理,更安全)。
- 无类型检查:观察者的
onNotify
方法接收通用QVariant
数据,需开发者自行保证类型匹配,易引入运行时错误。 - 线程不安全:默认实现未处理多线程场景,需额外添加互斥锁保护观察者列表。
适用场景:跨语言项目(如Qt与C#模块交互)、需要高度定制解耦逻辑的场景(如游戏中的角色状态变更通知)、轻量级模块通信(无需复杂事件类型管理)。
三、架构级解耦:分层与跨平台方案
5. MVC/MVVM —— 中大型应用的"分层蓝图"
核心特点:将应用划分为模型(Model)、视图(View)、控制器(Controller,MVC)或视图模型(ViewModel,MVVM),实现逻辑与界面的彻底分离。Qt对MVVM有天然支持(通过QAbstractItemModel
、QViewModel
等类)。
优势:
- 职责清晰:Model专注数据,View专注显示,ViewModel/Controller专注逻辑,团队可并行开发不同层。
- 可测试性强:Model和ViewModel可独立于UI进行单元测试(如用Qt Test框架测试数据计算逻辑)。
- 界面复用性高:同一ViewModel可绑定不同View(如桌面端用QWidget,移动端用QML),支持多端适配。
劣势:
- 学习成本高:需理解MVVM的双向绑定(Qt通过
QBinding
或第三方库如QtMvvm
实现)、数据驱动视图等概念。 - 额外代码量:需定义Model、ViewModel类,初期开发效率可能低于直接使用信号槽。
- 性能损耗:数据变更需通过信号槽传递,极端高频场景(如10kHz传感器数据)可能需优化(如批量更新)。
适用场景:中大型应用(如工业控制软件、医疗设备上位机)、需要多端适配(桌面+移动)、团队分工明确(UI组、逻辑组、数据组)。
6. 前后端分离(C++ + JS/Web)—— 跨平台的"终极解耦"
核心特点:将前端界面迁移至Web技术(HTML/JS/CSS),后端用C++实现高性能逻辑,通过HTTP/HTTPS或WebSocket通信。适合需要跨平台(Windows/macOS/Linux/Web)、界面复杂度高(如可视化图表)的场景。
技术选型对比:
技术方案 | 用途与优势 | 适用场景 |
---|---|---|
Cpp-REST SDK | 微软开发的RESTful服务框架,支持C++11及以上,集成简单 | 快速搭建HTTP API服务 |
Crow/Drogon | 高性能C++ Web框架(异步IO、模板引擎),性能接近Node.js | 高并发API服务(如传感器数据实时推送) |
gRPC + Protobuf | 谷歌的RPC框架,支持多语言,基于Protobuf序列化,性能优于HTTP/JSON | 低延迟、高可靠性内部通信(如跨进程调用) |
Boost.Beast | 低层HTTP/WebSocket库,需手动实现协议,灵活性高 | 需要高度定制通信协议的场景(如私有二进制协议) |
优势:
- 跨平台能力:前端可打包为桌面应用(Electron)、Web应用或移动应用(React Native),后端仅需一次开发。
- 界面自由度:利用Web技术(如Three.js、D3.js)实现复杂可视化(3D模型、实时曲线),远超Qt Widgets的能力边界。
- 团队协作友好:前端(JS)与后端(C++)团队可独立开发,通过API文档协作,降低沟通成本。
劣势:
- 通信延迟:HTTP协议存在握手开销(约10-100ms),不适合超高频数据传输(如1kHz传感器数据),需改用WebSocket或gRPC流。
- 部署复杂度:需维护后端服务(如启动C++服务器)、前端构建流程(如打包Electron应用),比纯Qt应用更复杂。
- 性能损耗:数据需序列化/反序列化(如JSON转二进制),高频通信场景(如实时控制)可能成为瓶颈。
适用场景:需要跨平台/跨语言的应用(如SaaS平台、工业物联网)、界面复杂度高(如数据可视化大屏)、团队包含前端开发人员。
四、多线程通信对比:线程安全的"最后一公里"
在多线程场景(如后台线程采集传感器数据,UI线程更新界面)中,需特别注意线程安全。以下是常见方案的线程安全性对比:
方法 | 跨线程能力 | 线程安全性 | 自动线程切换 | 推荐场景 |
---|---|---|---|---|
信号与槽(QueuedConnection) | ✅ | ✅(Qt自动管理) | ✅(切UI线程) | 后台线程通知UI(最常用) |
事件总线(MessageCenter) | ⚠️ | ❌(需手动加锁) | ❌ | 多模块同线程通信(如UI模块间) |
观察者模式 | ⚠️ | ❌(需手动同步) | ❌ | 不推荐直接用于多线程 |
发布-订阅(线程安全版) | ✅ | ✅(封装锁) | ❌(需自处理) | 多线程模块解耦(如日志分发) |
自定义QEvent + postEvent() | ✅✅ | ✅(Qt事件队列) | ❌(需手动投递) | 跨线程精准控制(如任务调度) |
MVC/MVVM | ⚠️ | ⚠️(依赖实现) | ❌ | 需配合线程模型(如后台Model) |
前后端分离(HTTPS) | ✅✅ | ✅(HTTP协议) | ❌(响应式处理) | 跨进程/跨机器多线程通信 |
关键原则:
- 跨线程通信优先用Qt原生机制(信号槽+QueuedConnection),避免手动操作线程API。
- 高频数据传输(如10kHz)避免用HTTP,改用WebSocket或共享内存(Qt的
QSharedMemory
)。 - 多线程事件总线需添加互斥锁(如
QMutex
),防止多线程并发发布导致的事件丢失。
五、架构选择建议:按项目阶段演进
根据项目规模、团队协作需求和功能复杂度,推荐以下架构演进路径:
项目阶段 | 推荐架构 | 关键技术栈 | 目标 |
---|---|---|---|
原型验证/小型工具 | 信号与槽 | Qt Widgets、信号槽基础 | 快速验证功能,降低初期开发成本 |
中型应用(10+界面) | 事件总线 + MVC/MVVM | 自定义MessageCenter、QAbstractItemModel | 解决UI与逻辑耦合,支持团队分工 |
大型复杂应用 | MVVM + 插件化架构 | Qt Meta-Object、动态库加载(QLibrary) | 支持功能扩展(如插件热插拔)、多模块协作 |
跨平台/跨语言应用 | 前后端分离(C++ + Web) | Crow/gRPC、Electron/React | 覆盖多终端(桌面/Web/移动),复用后端逻辑 |
总结
Qt架构的演进本质是解耦需求的升级:从简单的信号槽直连,到事件总线的全局广播,再到MVVM的分层设计,最终通过前后端分离实现跨平台能力。选择架构时需权衡:
- 小项目:追求开发效率,信号槽足够简洁;
- 中大型项目:重视可维护性,MVVM+事件总线是必选项;
- 跨平台项目:前后端分离虽增加复杂度,但能最大化复用后端逻辑。