4、模块化设计原则:高内聚低耦合、接口隔离原则、Qt模块划分实战
模块化设计,说白了就是「分而治之」。
我早年接手过一个老项目,整个工程就一个巨无霸的 .cpp 文件,两千多行。改一个按钮颜色,能编译五分钟。那感觉,就像在迷宫里修水管——牵一发动全身。后来我痛定思痛,开始认真研究模块化。今天就把这些经验掰开揉碎了讲给你听。
4.1 高内聚与低耦合:模块设计的黄金法则
这两个词你肯定听过。但到底怎么落地?
高内聚:一个模块只做一件事,并且把它做好。
低耦合:模块之间依赖越少越好,改一个模块不影响其他模块。
举个例子。假设你写一个网络下载模块。
- 高内聚:这个模块只负责「下载」。它不关心数据怎么解析,也不关心界面怎么显示。
- 低耦合:下载模块通过信号发射进度,界面模块通过槽函数接收。两者之间没有直接函数调用。
我在项目中遇到过一种典型反例:有人把数据库操作、网络请求、UI刷新全写在一个类里。结果呢?换一个数据库就要改整个类。这就是典型的「低内聚、高耦合」——灾难。
我的判断标准:如果你发现一个类里既有业务逻辑又有UI代码,那它大概率需要拆分了。
4.2 接口隔离原则:别让模块知道太多
接口隔离原则(ISP)是SOLID原则之一。它的核心思想是:客户端不应该依赖它不需要的接口。
你想想看,如果一个模块暴露了10个方法,但调用方只用其中2个,那剩下的8个就是「噪音」。不仅增加理解成本,还容易引发误调用。
在Qt中,我习惯用纯虚类来定义接口。比如:
// 定义下载接口
class IDownloader {
public:
virtual ~IDownloader() = default;
virtual void startDownload(const QString &url) = 0;
virtual void cancelDownload() = 0;
virtual bool isDownloading() const = 0;
signals:
virtual void downloadProgress(qint64 received, qint64 total) = 0;
virtual void downloadFinished(const QString &filePath) = 0;
virtual void downloadError(const QString &errorMsg) = 0;
};
这样,任何实现类只需要关注这3个方法和3个信号。调用方也只依赖这个接口,不关心底层是HTTP还是FTP。
我曾经踩过的坑:一开始图省事,把接口定义得特别「丰富」,结果每个实现类都要写一堆空方法。后来我学乖了——接口越小越好,不够再加,别一开始就堆功能。
4.3 Qt模块划分实战:一个下载器的例子
光说不练假把式。我们拿一个实际项目来拆解——多线程下载器。
我会把它分成以下几个模块:
| 模块名 | 职责 | 对外接口 |
|---|---|---|
| NetworkModule | 管理网络请求、重试、超时 | IDownloader 接口 |
| StorageModule | 文件写入、断点续传、校验 | IFileWriter 接口 |
| TaskModule | 任务队列、优先级、并发控制 | ITaskManager 接口 |
| UIModule | 进度显示、状态提示、用户操作 | 仅通过信号/槽与业务层通信 |
每个模块内部高度内聚,模块之间通过接口通信。比如TaskModule不直接调用NetworkModule的函数,而是通过信号触发:
// TaskModule 内部
void TaskModule::onStartTask(const QString &url) {
// 不直接调用 network->startDownload(url)
// 而是发射信号
emit requestDownload(url);
}
// 在 main.cpp 或装配器中连接
connect(taskModule, &TaskModule::requestDownload,
networkModule, &NetworkModule::startDownload);
这样做的好处是什么?
- 想换网络库?重写NetworkModule就行,其他模块不用动。
- 想加下载限速?只在NetworkModule内部改,不影响UI。
- 想支持多任务?TaskModule加个队列,NetworkModule完全不知情。
我个人的习惯:每个模块单独一个文件夹,里面放接口定义(.h)、实现(.cpp)、以及单元测试。这样找代码特别快。
4.4 模块划分的SVG结构图
下面这张图展示了我们刚才说的下载器模块划分:
4.5 避坑指南:我踩过的三个坑
讲完了理论,说说实战中容易翻车的地方。
- 过度拆分:我曾经把一个工具类拆成七八个文件,结果找函数比写代码还累。模块不是越细越好,够用就行。
- 接口膨胀:一开始就把接口设计得「完美」,结果后面需求一变,接口改得面目全非。我的建议是:先写实现,再提炼接口。
- 忽略信号/槽的性能:跨线程的信号/槽调用是异步的,如果频繁发射大量信号,可能造成UI卡顿。我一般会在业务层做节流(throttle),比如每200ms发射一次进度信号。
特别提醒:模块划分不是一蹴而就的。我通常会在项目初期先搭一个「粗糙」的模块结构,随着对业务的理解加深,再逐步重构。别怕改,怕的是不改。
好了,模块化设计这块就聊到这儿。记住三个关键词:接口隔离、高内聚、低耦合。下次写代码前,先画个模块图,问问自己:「这个类该不该存在?」
最后分享一个我自己的小技巧:每次新建一个类之前,先写三行注释——这个类的职责是什么、它依赖谁、谁依赖它。写不清楚,就别动工。