第 4 章 · 架构设计与模块化

需求定了,写第一行代码之前还有一个决定一切的问题:代码怎么组织。架构设计的对象是「模块的划分与模块间的关系」,它决定项目的天花板:好架构让后续每个功能「自然有地方放」,坏架构让三个月后的项目变成「每次改动都怕弄坏什么」的雷区。本章讲模块化的度量、经典架构模式、以及具体的设计决策方法。

4.1 模块化的两个度量:高内聚、低耦合

模块化的一切原则都是这两个词的展开:

  • 内聚(cohesion):模块内部的元素有多「属于同一个理由」。改「订单折扣计算」要动五个函数,它们全在一个模块里 = 高内聚;散在五处 = 低内聚;
  • 耦合(coupling):模块之间相互依赖的程度。A 直接读写 B 的全局变量 = 高耦合;A 只通过 B 的公开接口调用 = 低耦合。

目标:高内聚、低耦合。为什么?因为变更的代价 = 改动的模块数 × 每处的理解成本。高内聚让「一个变更只碰一个模块」;低耦合让「碰这个模块不会波及别的」。数据结构课第 2 章「链表节点自引用」、Java 课第 7 章「封装」,都是这对原则在不同尺度的化身。

一个实用的内聚检查法:试着用一句话描述模块的职责,如果要说「和」,拆;如果说不清楚,拆(单一职责原则的通俗版)。

4.2 分层架构:最经典的骨架

分层(layered)是使用率最高的架构:每层只调用下层,上层知道下层,反之不然:

表示层(UI/API 接口)
业务逻辑层(规则、流程)
数据访问层(数据库读写,数据库课)
数据库 / 外部服务

价值:依赖方向单一,测试和替换都简单(数据访问层换成 mock,业务层照样测)。Web 后端(MVC、三层架构)和这个 wiki 站(next 的 pages/components/lib 分目录)都是分层的变体。失效场景:层次太多导致「穿层透传」(一个字段改名的改动波及五层),层要少而清楚。

4.3 微服务与单体:规模决定形态

单体(monolith):所有功能一个进程一个部署单元。微服务(microservices):按业务域切成几十个独立服务,各自部署、各自的数据存储,服务间走网络调用(计网课第 10 章的 REST/RPC)。

单体微服务
开发/调试简单(一个进程)复杂(分布式调试)
部署整体部署独立部署,互不拖累
扩容整体扩热点服务单独扩
事务本地事务(数据库课第 7 章)分布式事务,难
团队少而全多团队并行

结论先行:小团队、早期产品,单体是正确答案。微服务解决的是「组织规模」问题(几十个团队不能改同一个代码库),不是技术问题;在没人需要独立部署时引入微服务 = 白吃分布式的所有苦(网络失败、最终一致、运维复杂度)。先用好模块化的单体,等某个模块的伸缩需求真实出现再拆,「先单体后拆分」是业内(包括亚马逊早期)走过的路。

4.4 接口设计:模块间的合同

模块间的边界靠接口(第 5 章设计模式的舞台)。好接口的判断标准:

  • 最小完备:调用方能完成全部任务,方法数又最少;
  • 抽象稳定:暴露「做什么」(getUserById),隐藏「怎么做」(SQL 还是缓存);
  • 错误语义清楚:失败时发生什么(抛什么异常/返回什么错误码,数据库课的事务语义在此对齐);
  • 向后兼容:新增参数有默认值,别让老调用方编译失败。

接口一旦有消费者就「冻结」了(改接口 = 协调所有消费者),所以接口设计的犯错成本远高于内部实现。经验法则:内部实现可以天天改,接口要当成对外的承诺来审慎设计。

4.5 架构决策记录(ADR):把「为什么」存档

架构决策(为什么用 PostgreSQL 不用 MongoDB、为什么单 体不上微服务)的推理过程,比决策本身更值钱(半年后有人来挑战时,当时的权衡是唯一辩护)。架构决策记录(ADR) 是轻量的解法:每个重大决策一份一页纸文档(背景 → 备选 → 决策 → 后果),放 git 里随代码演进。它治的是「祖传架构没人敢动、也没人知道为什么这么设计」的考古困境。

常见错误

  • 按技术层次切模块(所有 util 一堆、所有 model 一堆)。高内聚的反面:改一个业务要横跨 N 个「技术桶」;按业务功能切(feature-first)通常更好。
  • 过早微服务化。4.3 的重申;先让单体模块化,拆的接口都留好。
  • 循环依赖。A 依赖 B、B 依赖 A,构建工具开始报错;靠「抽公共下层」或「依赖倒置(第 5 章)」解环。
  • 上帝模块/上帝类。一个 Utils 里塞了 200 个函数、一个 Service 干所有事;4.1 的一句话描述测试通不过。

练习

  1. 动手:给你(或朋友的)一个项目画模块依赖图(节点=模块,箭头=import 方向),找出现环和「所有人都要 import 的万能模块」,写两条改进方案。
  2. 手算:某功能变更波及 8 个模块(高耦合),重构后波及 2 个(每次重构 3 人天)。若该功能每月变更 2 次,三个月的重构净收益是多少?
  3. 动手:为你的项目写第一份 ADR(一页纸):一个你纠结过的技术选型,老老实实列出当时的备选和理由,再看现在的自己是否同意。
  4. 思考:「分层架构中下层不得调用上层」这一规则,靠什么保证?编译器(依赖方向报错)、评审、还是自觉?对比 Java 课第 8 章的包可见性、以及 ArchUnit 这类「架构即测试」工具,思考「架构规则可执行化」的价值。