架构设计基础:分层架构、MVC/MVP/MVVM模式、C++项目目录结构规范

聊到架构设计,很多刚入行的朋友容易一头扎进设计模式里出不来。其实我个人觉得,比设计模式更先要搞清楚的,是架构的骨架——也就是分层和模式。骨架搭对了,后面填肉(写代码)才不容易散架。今天我们就来聊聊这三个核心话题:分层架构、MVC/MVP/MVVM 模式,以及 C++ 项目目录该怎么组织。

一、分层架构:把代码“隔开”的艺术

分层架构,说白了就是让不同职责的代码待在不同的房间里。你想想看,如果所有代码都堆在一个文件里,那改一个功能可能牵动全身,谁都不敢动。我早期做嵌入式项目时就吃过这个亏——一个 5000 行的 main.cpp,改个日志打印都能编译出链接错误……嗯,后来我彻底重构了。

经典的分层一般是三层:

  • 表现层(UI):负责和用户打交道,显示界面、接收输入。
  • 业务逻辑层(Service / Domain):核心算法、业务规则、状态管理。
  • 数据访问层(Persistence / Repository):读写数据库、文件、网络等。

每一层只依赖它的下一层,不能跨层调用。举个例子:UI 层不能直接操作数据库,必须通过业务层中转。这样做的好处是——哪天你想把数据库从 MySQL 换成 Redis,只需要改数据访问层,业务层和 UI 层完全不用动。

核心原则:上层依赖下层,下层不依赖上层。依赖关系是单向的。

我在项目中遇到过一种情况:团队为了“灵活”,让业务层直接返回 UI 层需要的 DTO 对象。结果 UI 层一改,业务层也得跟着改,分层形同虚设。所以记住:层与层之间用接口或抽象类通信,别让具体实现暴露出去。

二、MVC / MVP / MVVM:三种“分家”的姿势

这三兄弟都是分层架构的具体实现,但侧重点不同。我分别说说它们的核心区别,以及我在实际项目中的选择。

1. MVC(Model-View-Controller)

MVC 是最经典的。Model 负责数据和业务逻辑,View 负责显示,Controller 负责接收用户输入并协调 Model 和 View。它的流程是:用户操作 View → Controller 更新 Model → Model 通知 View 刷新。

但 MVC 有个坑:View 和 Model 之间容易产生直接依赖。我记得有一次,我们团队用 Qt 做桌面应用,View 直接绑定了 Model 的成员变量,结果 Model 一改,View 编译报错一大堆。后来我们强制要求 View 只能通过 Controller 访问 Model,才把耦合降下来。

我的建议:在 C++ 中实现 MVC,可以用信号槽机制(比如 Qt)或观察者模式来解耦 View 和 Model。但别让 View 直接持有 Model 的指针。

2. MVP(Model-View-Presenter)

MVP 把 Controller 换成了 Presenter。区别在于:View 和 Model 完全隔离,所有交互都通过 Presenter 进行。View 只负责显示和事件转发,Presenter 负责业务逻辑和状态管理。

我曾在 Windows 桌面应用(MFC)中用过 MVP。当时界面逻辑很复杂,但测试起来特别爽——因为 Presenter 不依赖具体的 View 实现,只需要一个接口。写单元测试时,我 mock 一个 View 接口,就能把业务逻辑测个遍。

// 一个简单的 MVP 示例(C++ 伪代码)
class IView {
public:
    virtual void ShowData(const std::string& data) = 0;
    virtual ~IView() = default;
};

class Presenter {
public:
    Presenter(IView* view, Model* model) : view_(view), model_(model) {}
    void OnUserClick() {
        std::string result = model_->Process();
        view_->ShowData(result);
    }
private:
    IView* view_;
    Model* model_;
};

3. MVVM(Model-View-ViewModel)

MVVM 是 MVP 的进化版,核心是 数据绑定。ViewModel 暴露可观察的属性,View 自动订阅这些属性的变化。这样 View 几乎不需要写代码,只需要在 UI 里绑定 ViewModel 的属性即可。

MVVM 在 C++ 中实现起来稍微麻烦一些,因为 C++ 没有原生的数据绑定机制。不过可以用 Qt 的 Q_PROPERTY 或 Boost.Signals2 来模拟。我个人觉得,如果项目用了 Qt Quick(QML),MVVM 是天然的选择;如果是纯 Widget 项目,MVP 反而更可控。

注意:MVVM 的数据绑定如果滥用,会导致调试困难。我曾经遇到过一个 bug:界面某个字段不更新,查了半天发现是 ViewModel 的属性名拼写错了,但编译期完全检查不出来。所以建议绑定逻辑尽量集中,别散得到处都是。

三、C++ 项目目录结构规范

架构模式定好了,代码往哪放?目录结构就是项目的“物理架构”。我见过太多 C++ 项目,所有源文件堆在一个 src 目录下,头文件和实现混在一起,找文件全靠搜索。嗯,这种项目维护起来真的很痛苦。

下面是我个人比较推荐的一种目录结构,适用于中型以上 C++ 项目:

project_root/
├── 3rdparty/          # 第三方库(只放头文件和静态库)
├── build/             # 构建输出目录(CMake 构建)
├── cmake/             # CMake 模块和工具脚本
├── docs/              # 文档(设计文档、API 文档)
├── include/           # 公共头文件(对外暴露的接口)
│   └── module_name/
│       ├── public_api.h
│       └── ...
├── src/               # 源代码
│   ├── module_a/      # 模块 A
│   │   ├── impl/      # 内部实现(不对外暴露)
│   │   ├── test/      # 模块 A 的单元测试
│   │   └── CMakeLists.txt
│   ├── module_b/
│   │   ├── impl/
│   │   ├── test/
│   │   └── CMakeLists.txt
│   └── main.cpp       # 主入口
├── test/              # 集成测试 / 端到端测试
├── tools/             # 辅助工具(代码生成、脚本)
├── .clang-format      # 代码格式化配置
├── .gitignore
└── CMakeLists.txt     # 顶层 CMake 文件

几个关键点:

  • 头文件与实现分离:公共头文件放在 include/,内部实现放在 src/模块名/impl/。这样外部使用者只看到接口,看不到实现细节。
  • 模块化:每个模块有自己的目录,包含独立的 CMakeLists.txt。模块之间通过接口依赖,不要直接引用其他模块的内部头文件。
  • 测试与源码同级:每个模块的单元测试放在模块目录下的 test/ 中,方便开发者快速定位和运行。
  • 第三方库统一管理:所有第三方库放在 3rdparty/,不要散落在系统路径中。这样换机器或 CI 环境时,只需要拷贝这个目录。
避坑指南:我曾经在一个项目里看到有人把 3rdpartysrc 混在一起,结果升级第三方库时,不小心把旧版本的头文件删了,导致整个项目编译失败。所以第三方库一定要独立出来,并且用版本号或 git submodule 管理。

四、一张图看懂本章核心

下面我用一张 SVG 图来总结分层架构、MVC/MVP/MVVM 的关系,以及它们如何映射到 C++ 项目目录中。这张图我画得比较简洁,但核心逻辑都在里面了。

架构模式与目录结构映射 表现层 (UI) 业务逻辑层 (Service) 数据访问层 (Repository) MVC: View ↔ Controller ↔ Model MVP: View ↔ Presenter ↔ Model MVVM: View ↔ ViewModel ↔ Model 目录结构 include/ module_a/ module_b/ src/ module_a/impl/ module_a/test/ module_b/impl/ module_b/test/ 分层架构是骨架,MVC/MVP/MVVM 是骨架上的关节,目录结构是肌肉的分布

五、总结与个人体会

架构设计没有银弹。分层架构、MVC/MVP/MVVM 都是工具,关键看你的项目场景。我个人习惯:

  • 如果是桌面应用(Qt Widgets),我倾向 MVP,因为 Presenter 容易测试,View 的替换成本低。
  • 如果是移动端或 QML 项目,MVVM 更自然,数据绑定能省很多代码。
  • 如果是后端服务,分层架构足够,配合依赖注入框架(比如 Boost.DI)效果更好。

至于目录结构,我建议团队在项目初期就定好规范,并且用 CMake 的 add_subdirectory 强制模块化。别等到代码写到 10 万行再重构——那时候成本就高了。

一句话总结:架构是约束,也是自由。好的架构让你在修改时只改一处,坏的架构让你改一处却要测十处。

好了,这一章的内容就到这里。下一章我们会深入聊聊设计模式在 C++ 中的具体应用,到时候我会拿几个真实项目中的例子来拆解。嗯,敬请期待。


公众号:蓝海资料掘金营,微信 deep3321