软件项目开发全流程 · 第 9 课 · 系统架构设计:模块、边界与契约
1、架构不是画图,是"最难改的决策"
写错一个函数,改它 10 分钟;选错一个架构,改它 3 个月。
架构(Architecture)是系统的骨架与规则:分哪些模块、谁依赖谁、数据怎么流、外部怎么接入。它的本质特征是——决定一旦定下,后续所有代码都要绕着它写,改起来的代价极高。
所以架构设计的目标不是"看起来高级",而是:
- 满足当前的质量属性要求(性能、可维护、安全、可扩展)
- 让未来的改动尽量便宜(可演进)
- 让团队能并行工作(边界清晰)
一句话:架构是为"变化"留的余地。
2、先定质量属性:非功能需求驱动架构
架构不是凭空设计的,它由质量属性(Quality Attributes)驱动。同一个功能,在不同质量要求下,架构完全不同:
| 质量属性 | 关键问题 | 对架构的影响 |
|---|---|---|
| 性能 | 响应要几毫秒?峰值 QPS? | 要不要缓存、要不要异步 |
| 可扩展性 | 一年后用户翻 10 倍怎么办? | 能否水平扩展、有无状态 |
| 可维护性 | 团队 3 人会变 10 人吗? | 模块边界、可测试性 |
| 可用性 | 允许停多久? | 冗余、故障隔离、降级 |
| 安全 | 数据敏感吗?谁能访问? | 鉴权位置、加密边界 |
| 成本 | 预算多少? | 自建 vs 云服务、单体 vs 微服务 |
做法:为每个质量属性写一个可度量的场景:
当并发用户达到 500 时,任务列表接口响应时间 P95 ≤ 800ms。
问不出量化场景,说明这个属性根本没人真正在乎——不要为假想的"未来百万用户"过度设计。
3、模块划分:高内聚、低耦合
模块划分只有两条铁律:
- 高内聚(High Cohesion):一个模块内部的东西"是同一件事"。
- 低耦合(Low Coupling):模块之间尽量少知道对方的事。
怎么判断划分好不好? 问三个问题:
- 我改这个功能,要动几个模块?(动得越少越好,最好 1 个)
- 这个模块能不能单独测试?(依赖太多就难测)
- 新人要理解它,需要同时看懂几个模块?(越少越好)
一个实用的划分方法:按"变化的原因"切,而不是按"技术类型"切。
❌ 反例(按技术切):
controllers/ ← 所有业务的入口都在这
services/ ← 所有业务的逻辑都在这
models/ ← 所有数据都在这一坨结果:改"任务"功能,要在三个目录里各改一处,且这三个目录里混着"用户""通知""统计"的各种代码。
✅ 正例(按业务能力切):
task/ (Task 的入口+逻辑+数据,自成一体)
user/
notify/4、分层架构与依赖方向
单个模块内部,常用分层组织:
┌─────────────────────────────┐
│ 接口层(API / Controller) │ 接收请求、参数校验、返回结果
├─────────────────────────────┤
│ 应用层(Service / UseCase) │ 编排流程、事务边界
├─────────────────────────────┤
│ 领域层(Domain / Model) │ 核心业务规则(最稳定)
├─────────────────────────────┤
│ 基础设施层(Repo / Client) │ 数据库、外部服务、消息
└─────────────────────────────┘最关键的一条规则:依赖只能指向内层(领域),不能反向。
- ❌ 领域层里
import数据库客户端 → 业务逻辑被技术绑死,测试必须连数据库。 - ✅ 领域层只定义"我要存取什么"(接口),由基础设施层去实现(依赖倒置)。
这条规则带来的直接好处:核心业务规则可以脱离数据库、脱离 HTTP 单独测试——这正是第 20 课(测试)能高效开展的前提。
5、接口契约:模块之间怎么说话
模块边界要用契约(Contract)固定,而不是"直接调对方内部函数"。
契约要写清三件事:
- 输入:字段、类型、必填性、取值范围
- 输出:成功返回什么、失败返回什么(错误码/异常)
- 不变量:调用前后系统保证什么(幂等?有序?)
契约的三个层次:
| 层次 | 形式 | 谁关心 |
|---|---|---|
| 内部模块间 | 函数签名 / Trait / Interface | 开发 |
| 服务间 | REST / gRPC / 消息 schema | 前后端、跨团队 |
| 外部系统 | 开放 API 文档 | 第三方 |
契约一旦发布,改它就是"破坏性变更"——要加版本、要公告、要过渡期。这正是第 31 课"兼容性与废弃策略"的伏笔。
6、常见架构风格与权衡
| 风格 | 适合 | 代价 |
|---|---|---|
| 分层单体 | 中小项目、团队小、需求变化快 | 规模大了会变"大泥球",需靠模块纪律维持 |
| 模块化单体 | 想保留单体部署简单,又要清晰边界 | 需要强制的模块访问规则(否则边界会烂) |
| 六边形 / 整洁架构 | 业务复杂、外部依赖多、要可测试 | 初期代码量变多、抽象层多 |
| 事件驱动 | 异步解耦、削峰、多消费者 | 时序难追踪、最终一致难调试 |
| 微服务 | 多团队独立交付、独立伸缩 | 分布式复杂性、运维成本极高 |
一条铁律:不要用微服务解决"代码乱"的问题。 代码乱是模块划分问题,拆成微服务只会让它变成分布式的大泥球——而且更难修。
默认建议:中小团队优先模块化单体(Modular Monolith)——一个部署单元,内部边界清晰,等某个模块真的需要独立伸缩或独立交付时,再把它拆出去。这条路是可逆的。
7、演进式架构:敏捷里的架构怎么做
传统做法是"架构设计先行,一次定稿"。敏捷里更现实的做法是演进式架构(Evolutionary Architecture):
- "最后一刻负责"(Last Responsible Moment):能推迟的决策就推迟,等信息足够再决定——比如"用哪个消息队列",在真要用之前不必定死。
- 架构守护(Fitness Function):用自动化规则约束架构不腐化。例如 CI 里检查"领域层不得依赖基础设施层",违规即构建失败。
- 每个迭代还一点架构债:重大结构改造拆成小步,随迭代推进,而不是攒到最后大重构。
- 用 Spike 验证风险:拿不准的技术方案,花 1~2 天做技术探针(Spike),先写最小验证代码,用结果说话(第 11 课展开)。
敏捷不是不要架构,而是架构要与代码一起长,且每一步都可运行、可验证。
8、架构评审与产出物
架构要有能评审的产出物,而不是"某个大佬脑子里的想法"。
最小产出物清单:
- 模块边界图(谁依赖谁,用箭头画出来)
- 关键流程时序图(一个核心场景的调用链)
- 数据流图(数据从哪来、存哪、去哪)
- 质量属性场景表(可度量的非功能需求)
- ADR 清单(关键决策及其理由,第 11 课)
评审要问的五个问题:
- 最大的技术风险是什么?有没有验证?
- 哪个模块将来最可能变?变化时波及面多大?
- 有没有循环依赖?
- 出故障时,影响范围能限制住吗?
- 新人多久能跑起来这个项目?
9、动手实验 🎯
任务:为「团队任务清单」设计一版架构。
- 写出 5 条质量属性场景(如"500 并发下 P95 ≤ 800ms"),并说明它如何影响设计。
- 画一张 模块边界图(ASCII 或纸笔均可),标出依赖方向,检查有无循环依赖。
- 为 1 个核心流程(创建任务并触发提醒)画时序图,标出契约(输入/输出/失败处理)。
- 选一个你真实的小项目,画出当前的架构,判断它属于哪种风格(分层单体/模块化单体/…)。
- 用"高内聚低耦合"三问做一次自检,找出 1 个最该拆开的模块,写出拆分方案。
- 写一条架构守护规则(例:领域层不得依赖任何数据库客户端),并说明怎么在 CI 里自动检查。
做完这步,你就有了"能评审、能落地、能守住"的架构骨架——下一课专门解决最难回头的一块:数据设计。
10、今天你带走的三句话
- 架构由质量属性驱动:写不出可度量的非功能需求,就不要开始谈架构。
- 高内聚低耦合 = 按"变化的原因"切模块,依赖方向只能指向核心业务,不能反向。
- 优先模块化单体,别用微服务治代码乱;架构要能演进,用 Spike 和架构守护来控制风险。
下一课进入数据设计:选型与建模——系统里最难改、也最该一次想清楚的部分。