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结构图

下面这张图展示了我们刚才说的下载器模块划分:

多线程下载器 - 模块划分图 NetworkModule 网络请求/重试 StorageModule 文件写入/校验 TaskModule 任务队列/并发 UIModule 进度/状态显示 信号: requestDownload 信号: saveFile 信号: progress/status 接口: IDownloader 接口: IFileWriter 模块之间仅通过接口和信号通信,实现高内聚低耦合

4.5 避坑指南:我踩过的三个坑

讲完了理论,说说实战中容易翻车的地方。

  1. 过度拆分:我曾经把一个工具类拆成七八个文件,结果找函数比写代码还累。模块不是越细越好,够用就行。
  2. 接口膨胀:一开始就把接口设计得「完美」,结果后面需求一变,接口改得面目全非。我的建议是:先写实现,再提炼接口。
  3. 忽略信号/槽的性能:跨线程的信号/槽调用是异步的,如果频繁发射大量信号,可能造成UI卡顿。我一般会在业务层做节流(throttle),比如每200ms发射一次进度信号。

特别提醒:模块划分不是一蹴而就的。我通常会在项目初期先搭一个「粗糙」的模块结构,随着对业务的理解加深,再逐步重构。别怕改,怕的是不改。

好了,模块化设计这块就聊到这儿。记住三个关键词:接口隔离、高内聚、低耦合。下次写代码前,先画个模块图,问问自己:「这个类该不该存在?」

最后分享一个我自己的小技巧:每次新建一个类之前,先写三行注释——这个类的职责是什么、它依赖谁、谁依赖它。写不清楚,就别动工。

可可精品资料库