模块化设计:高内聚低耦合的工程实践
模块化设计,说白了就是「分而治之」。我做了这么多年Qt项目,最深的体会就是:模块化搞不好,后期全是坑。今天咱们就聊聊,怎么把Qt项目拆得漂亮、搭得结实。
模块化原则:高内聚与低耦合
这两个词你肯定听过,但真正理解的人不多。我简单解释一下:
- 高内聚:一个模块只做一件事,并且把它做好。比如一个网络模块,就别掺和UI逻辑。
- 低耦合:模块之间依赖越少越好。改了一个模块,其他模块最好不用动。
我在项目中遇到过这样的案例:有个同事把数据库操作、网络请求、界面刷新全写在一个类里。结果每次改需求,那个文件就几百行地改。嗯,后来重构花了三倍时间。
核心原则:模块对外暴露的接口越少,耦合度越低。能用3个公开方法解决的,别用5个。
Qt模块划分策略
Qt项目怎么划分模块?我一般按这个思路来:
| 层次 | 模块 | 职责 | 依赖关系 |
|---|---|---|---|
| UI层 | Widgets / QML | 界面展示、用户交互 | 依赖业务层 |
| 业务层 | Core / Logic | 业务流程、状态管理 | 依赖数据层 |
| 数据层 | Data / Model | 数据存取、网络通信 | 无业务依赖 |
| 基础层 | Common / Utils | 工具函数、通用组件 | 无任何依赖 |
你想想看,这种分层的好处是什么?每一层都可以独立测试、独立替换。比如你想把数据库从SQLite换成MySQL,只需要改数据层,UI层完全不用动。
我的习惯:每个模块单独一个.pro文件,编译成静态库或动态库。这样编译时只编译改动的模块,速度能快不少。
接口设计:信号槽 vs 回调
这是Qt里一个经典话题。我两种都用过,说说我的感受。
信号槽的优势
- 松耦合:发送者不知道接收者是谁,接收者也不知道谁在发信号
- 线程安全:Qt的信号槽支持跨线程调用,自动处理事件循环
- 调试方便:可以用QSignalSpy做单元测试
回调的适用场景
- 性能敏感:信号槽有额外开销,高频调用时回调更合适
- 简单场景:比如一个按钮点击,用lambda回调比信号槽更简洁
// 信号槽方式
class DataLoader : public QObject {
Q_OBJECT
signals:
void dataReady(const QByteArray &data);
public:
void load() {
// 异步加载数据
emit dataReady(result);
}
};
// 回调方式(适合简单场景)
class DataLoader {
public:
using Callback = std::function<void(const QByteArray&)>;
void load(Callback cb) {
// 加载完成后调用cb
cb(result);
}
};
我曾经踩过的坑:在性能关键路径上用了大量信号槽,结果界面卡顿。后来换成回调,性能提升了30%。所以我的建议是:UI事件用信号槽,计算密集型用回调。
依赖注入与工厂模式
这两个模式在Qt项目里特别实用。说白了就是:别让类自己new东西,让别人给你送过来。
依赖注入
我习惯用构造函数注入,简单明了:
class UserService {
public:
// 依赖通过构造函数注入
UserService(Database *db, NetworkClient *client)
: m_db(db), m_client(client) {}
void login(const QString &username, const QString &password) {
// 使用注入的对象
m_db->query(...);
m_client->post(...);
}
private:
Database *m_db;
NetworkClient *m_client;
};
这样做的好处是:测试时传个Mock对象进去就行。我在项目中用Google Mock配合依赖注入,单元测试覆盖率从30%提到了80%。
工厂模式
当对象创建逻辑复杂时,工厂模式就派上用场了:
class WidgetFactory {
public:
static QWidget *createWidget(const QString &type) {
if (type == "button") {
return new QPushButton("按钮");
} else if (type == "label") {
return new QLabel("标签");
} else if (type == "slider") {
return new QSlider(Qt::Horizontal);
}
return nullptr;
}
};
我的建议:把工厂模式和依赖注入结合起来用。工厂负责创建对象,依赖注入负责把对象传给需要的地方。这样整个项目的依赖关系就清晰了。
知识体系总览
下面这张图,是我对模块化设计的整体理解:
这张图把模块化设计的核心要素都串起来了。从原则到实现手段,再到最终目标,每一步都很清楚。
一个小技巧:开始写代码前,先画一张类似的依赖关系图。哪个模块依赖哪个,一目了然。我每次重构前都会画,能避免很多后期问题。
好了,模块化设计就聊到这儿。记住一句话:好的模块化设计,是改一处代码不用改十处。下次你写Qt项目时,不妨试试这些方法。