news 2026/8/26 17:49:58

Aether 项目 3 年重构 4 次,我学到的 5 个教训

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Aether 项目 3 年重构 4 次,我学到的 5 个教训

从"能跑就行"到"架构清晰",工业项目的进化之路


一、一个真实的病历本

第一版 Aether:一个巨大的 main.cpp,所有代码堆在一起。

第二版 Aether:分出了 common/app/plugins,但耦合依然严重。

第三版 Aether:引入了 IoC 容器,插件间终于解耦了。

第四版 Aether:完整的 MVVM + 中间件 + 权限 + 主题。

每次重构都像在动手术。但每次手术后,项目都活得更好了。

这不是教你如何写出完美架构的文章。这是把 3 年挨过的刀、踩过的坑、流过的血,一条条摆在你面前。


二、V1 → V2:从单体到分层

改一个 bug 修 3 天

V1 的 Aether,核心逻辑全在一个main.cpp里,2 万多行。

// V1 main.cpp(节选)#include<QApplication>#include<QTcpSocket>#include<QSerialPort>#include<QCamera>#include<QSqlDatabase>// 全局变量大集合QCamera*g_camera=nullptr;QTcpSocket*g_comm=nullptr;QSqlDatabase g_db;intg_currentMode=0;// 2 万行代码,全部平铺// 相机初始化、通信协议解析、界面更新、// 数据库写入、异常处理...全混在一起

问题在哪?改一行g_comm的协议解析,相机也跟着崩。因为全局变量谁都改,谁都不清楚改完的影响范围。

有一次修"界面卡死"的 bug,定位到g_comm->readAll()阻塞了 UI 线程。但通信、界面、数据库都在同一个函数里互相调用。改 1 个 bug,用了 3 天。因为不敢动,动一处要验证所有地方没炸。

分层后效率翻倍

V2 我们做了第一件正确的事:按职责分层。

V1 V2 aether/ aether/ └── main.cpp ← 2万行 ├── app/main.cpp ├── common/ │ ├── comm/ ← 通信模块 │ ├── camera/ ← 相机模块 │ └── database/ ← 数据库模块 └── plugins/ui/ ← 界面模块

每个模块有自己的文件夹、命名空间、编译单元。模块之间通过接口调用,不碰对方的全局变量。

// V2 common/comm/comm_manager.h// 通信模块对外只暴露这一个接口classCommManager:publicQObject{Q_OBJECTpublic:boolsend(constQByteArray&data);voidonReceived(std::function<void(constQByteArray&)>cb);};

以前改通信,要在 2 万行里找所有相关代码。现在改通信,只打开common/comm/就行。

效果:原来 3 个人改同一个文件天天冲突。分层后并行开发,6 周的工作 2 周交付。


三、V2 → V3:从分层到插件化

分层也没解决耦合

分层之后,新问题冒头了——模块间依赖还是太强。

改了camera_manager.cpp的参数类型,comm/ui/各有三处调用了它,全要跟着改。

// V2 的问题:模块间直接调用,耦合太深voidCommManager::onDataReceived(constQByteArray&data){automsg=parse(data);// 直接调用 database 模块的接口DatabaseManager::instance().saveRecord(msg);// 直接调用 ui 模块的接口UIManager::instance().updateDisplay(msg);}

分层的思路对了,但层与层之间还是硬编码耦合。

插件化:痛在接口设计

V3 引入插件架构:每个模块编译成独立 DLL,通过接口交互。

// V3 通信插件接口// plugins/interface/icommunication.hclassICommunication:publicQObject{Q_OBJECTpublic:virtual~ICommunication()=default;virtualboolconnect(constQString&endpoint)=0;virtualboolsend(constQByteArray&data)=0;virtualboolisConnected()const=0;signals:virtualvoiddataReceived(constQByteArray&data)=0;};

最大的痛苦是接口设计。第一版ICommunication有 12 个方法。结果每个插件(TCP、串口、CAN)只实现了其中 6 个,剩下 6 个写return false

接口太胖,插件架构就废了。后来拆成 3 个小接口,各取所需。


四、V3 → V4:从插件到 IoC + MVVM

插件间还是太依赖

插件化解决了"改一处崩全局"。但新问题来了——插件 A 依赖插件 B 的实例,加载顺序成了玄学。

// V3 的痛:加载顺序玄学auto*b=pluginManager->getPlugin<IPluginB>();if(!b)return;// 插件 B 还没加载b->doSomething();

今天能跑,明天加个新插件,加载顺序变了又崩。

IoC 容器:真正解耦

V4 的核心变化:引入 IoC 容器,你需要的服务,容器给你注入,不用自己找。

// V4 - 容器自动注入依赖classServiceA:publicQObject{public:explicitServiceA(IServiceB*serviceB):m_serviceB(serviceB)// ← 容器自动注入{// 直接使用,不用关心 serviceB 什么时候初始化的connect(m_serviceB,&IServiceB::onEvent,this,&ServiceA::handleEvent);}private:IServiceB*m_serviceB=nullptr;};

容器带来三个改变:不用关心加载顺序(容器按依赖图排序)、不用硬编码实现(换实现只改注册)、测试友好(注册 Mock 即可)。

MVVM 同时解决了 UI 和业务层的耦合:

// V4 - ViewModel 层classMainViewModel:publicQObject{Q_PROPERTY(QString status READ status NOTIFY statusChanged)public:explicitMainViewModel(ICameraService*camera,ICommService*comm):m_camera(camera),m_comm(comm){}Q_INVOKABLEvoidstartCapture(){m_camera->start();m_status="采集中...";emitstatusChanged();}private:ICameraService*m_camera;ICommService*m_comm;QString m_status;};

View 只绑 ViewModel 的属性,ViewModel 通过容器拿服务。每个维度都解耦了。


五、5 个用血泪换来的教训

教训 1:不要过早抽象

V1 到 V2,我们最大的冲动是"先把框架搭好"。搭了一个极其通用的数据总线架构,设计了 8 个抽象层。结果业务写了两行发现用不上,绕开框架直接走硬编码。

抽象不是设计出来的,是从代码里长出来的。先写能跑的代码,再抽公共逻辑。V2 到 V3 我们换了这个策略——先在业务里发现重复模式,再抽接口。这次对了。

教训 2:接口变动要发版

V3 的插件接口改得很随意。今天加个方法,明天改返回值。插件开发者天天追着接口改,一个接口改了 5 次,6 个插件崩了 4 次。

后来强推语义化版本:

icommunication v1.0.0 ← 稳定版,不能改 icommunication v1.1.0 ← 加新方法,不破已有签名 icommunication v2.0.0 ← 破坏性变更,全员确认

接口变动必须发版、写变更日志、通知所有消费者。这规矩定了之后,插件间的地震几乎绝迹。

教训 3:架构文档和代码同步

V2 到 V3 踩过最大的坑——代码重构了,文档没更新。新同事拿着 V2 的文档理解 V3 的代码,前两个月全在考古中度过。

我们的解法:架构图放进代码仓库、和代码一起 PR、每次重构先改文档再改代码。

教训 4:重构时测试先行

V2 到 V3 那次重构,我们没写测试就直接上。结果是——每个接口都能工作,但流程跑起来到处断链。

不写测试的重构不叫重构,叫赌博。

后来定了死规矩:重构前先给现有代码写测试,锁定接口行为。重构完跑一遍,全绿才算通过。

// 重构前锁定行为TEST_CASE("通信模块发送数据"){CommManager comm;REQUIRE(comm.send(QByteArray())==false);// 空数据返回 falseREQUIRE(comm.send(QByteArray("hello"))==true);// 合法数据返回 trueboolsignalReceived=false;QObject::connect(&comm,&CommManager::dataSent,[&](){signalReceived=true;});comm.send(QByteArray("hello"));REQUIRE(signalReceived==true);}

教训 5:渐进式重构 > 推倒重来

4 次重构里最失败的是 V2 中期的"大重写"。花了 3 个月写了 80% 的新代码,然后发现——业务已经变了。3 个月白干。

推倒重来永远是最后的选择。正确的做法是用 Strangler Fig 模式:

  1. 在旧系统旁建新模块
  2. 新功能走新模块
  3. 逐步迁移老功能
  4. 老功能没人用了再拆掉

V3 到 V4 这么干的——IoC 容器和旧架构共存了 2 个月,等所有插件迁移完才删旧代码。

没有切换日,只有淘汰日。


六、现在的 Aether

┌────────────────────────────────────┐ │ View 层 (QML/QWidget) │ │ 只做展示,不写业务 │ ├────────────────────────────────────┤ │ ViewModel 层 │ │ 属性绑定 + 命令调度 │ ├────────────────────────────────────┤ │ Service 层 (IoC 容器管理) │ │ 相机 · 通信 · 数据服务 │ ├────────────────────────────────────┤ │ 插件层 (独立 DLL,接口交互) │ │ core · mvvm · container · home │ └────────────────────────────────────┘

每一层只关心一件事,每一层都可以独立替换。

3 年,4 次重构,从一个 2 万行的 main.cpp 到今天。

如果你问我最大的感受是什么?

没有一劳永逸的架构,只有不断进化的代码。

下一次重构什么时候来?我不知道。但只要项目还活着,重构就不会停。


💬 评论区聊聊:

你的项目重构过几次?最大的教训是什么?是接口设计翻车了,还是测试没跟上?评论区说说你的经历。

🔄 觉得有用?

分享给你的团队——这些教训,一个人用血泪换来,十个人看到就能少走弯路。

⭐ 点个"在看"让更多人看到,也鼓励我继续写下去。


下一期预告:

下一篇,从 Windows 到 Linux——跨平台适配。
你以为 Qt 是跨平台的?光是文件路径的斜杠方向,就够你喝一壶的。
下篇聊聊 Aether 跨平台适配的真实经历。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/26 17:48:50

DeepSeek Harness Headless 模式:把 AI Agent 写进 CI/CD 流水线

DeepSeek Harness Headless 模式&#xff1a;把 AI Agent 写进 CI/CD 流水线系列导航&#xff1a;本篇是 DeepSeek Harness 实战系列第 4 篇。前面讲了编码实战、框架对比、会话日志。本文聚焦一个把 DSH "产品化"的关键能力——Headless&#xff08;无头&#xff09…

作者头像 李华
网站建设 2026/8/26 17:48:14

企业 AI 真正缺的,可能不是本体,而是理解业务世界的方法

如果让 AI 真正理解一家企业&#xff0c;到底需要什么&#xff1f;一开始很容易想到的是&#xff1a;数据治理、本体、知识图谱、RAG、MCP……但继续往下推&#xff0c;会发现一个更基础的问题&#xff1a;我们甚至还没有真正把「这个企业的业务世界是什么」说清楚。ERP 里有订…

作者头像 李华
网站建设 2026/8/26 17:48:11

Python实现大模型API负载均衡的8种方法(第7种最省成本)

第一章在构建高性能的 AI 服务系统之际, 大模型 API 的调用常常遇上高并发挑战, 还面临低延迟状况以及稳定性问题, 负载均衡身为分布式系统里最为核心重要的技术当中的一个, 它能够切实有效地去分配请求流量, 进而避免单点出现过载情形, 并且能提升整体服务的可用性以及响应效率…

作者头像 李华
网站建设 2026/8/26 17:46:20

芝士算法(模拟)

目录 替换所有问号 提莫攻击 Z字形变换 外观数列 数青蛙 替换所有问号 替换所有问号 public String modifyString(String s) {char[] s1 s.toCharArray();int n s.length();for(int i 0 ; i < n; i){if(s1[i] ?){for(char ch a ; ch < z; ch ){if((i 0 || c…

作者头像 李华
网站建设 2026/8/26 17:41:28

Python进阶教程:15.2_OpenAI 库 —— 全方位使用指南

本文接上期&#xff1a;15.1_OpenAI 库 —— 全方位使用指南继续&#xff1a;八、文字转语音&#xff08;TTS&#xff09;8.1 基本用法from openai import OpenAI client OpenAI()# 把文字变成语音 response client.audio.speech.create(model"tts-1", # …

作者头像 李华