3. 系统设计基础:模块化设计原则、高内聚低耦合、接口设计规范
好,咱们进入第三个章节。说实话,系统设计这块,是区分「码农」和「工程师」的一道分水岭。你代码写得再溜,如果系统设计一塌糊涂,那后面维护起来,真的是欲哭无泪。我个人习惯,在动手写第一行代码之前,至少花 30% 的时间在纸面上画模块、定接口。
这一章,咱们就聊聊系统设计的三个核心支柱:模块化、高内聚低耦合、以及接口设计。说白了,就是教你如何把一个大系统拆得漂亮,还能让各个零件配合得天衣无缝。
3.1 模块化设计原则:拆的艺术
模块化,就是把一个复杂系统拆分成若干个独立的、可替换的模块。为什么要拆?你想想看,一个 10 万行代码的文件,谁敢动?但如果你把它拆成 100 个 1000 行的模块,每个模块负责一件明确的事,那维护起来就轻松多了。
这里有几个我一直在用的原则:
- 单一职责原则:一个模块只做一件事,并且把它做好。我在项目中遇到过那种「万能模块」,既处理用户登录,又负责生成报表,还兼职发送邮件。结果呢?改登录逻辑,报表崩了。千万别这么干。
- 接口隔离原则:模块对外暴露的接口要尽量精简。不要为了图方便,把一堆用不上的方法都丢出去。接口越胖,依赖越重,耦合越深。
- 依赖倒置原则:高层模块不应该依赖低层模块,两者都应该依赖抽象。嗯,这里要注意,这是解耦的关键。你依赖一个具体的数据库驱动,将来换数据库就得改代码;但如果你依赖一个抽象的「数据存储接口」,那换数据库就只是换个实现而已。
3.2 高内聚与低耦合:系统设计的黄金法则
这两个词,你肯定听得耳朵起茧了。但真正理解并做到的人,其实不多。我简单解释一下:
- 高内聚:模块内部的元素(函数、数据)之间,关联要紧密。说白了,就是「跟自己人抱团」。一个负责订单处理的模块,里面所有函数都应该围绕「订单」这个核心概念转,别把用户管理的逻辑也塞进来。
- 低耦合:模块与模块之间的依赖关系要弱。说白了,就是「跟外人保持距离」。一个模块出问题,尽量不要影响到其他模块。
为什么会这么重要?我记得有一次,我接手一个遗留系统。那个系统里,模块 A 直接调用了模块 B 的私有变量,模块 B 又直接操作了模块 C 的数据库表。结果呢?改一个 bug,牵出三个新 bug。这就是典型的「高耦合」灾难现场。
下面这张图,可以帮你直观理解:
3.3 接口设计规范:模块间的契约
接口,就是模块之间签订的「合同」。合同写得清楚,大家合作愉快;合同写得模糊,后面全是扯皮。我曾经因为一个接口的参数顺序问题,跟另一个团队吵了整整一下午。从那以后,我对接口设计就特别较真。
下面是我总结的几个接口设计规范:
3.3.1 接口要稳定且明确
接口一旦对外发布,就不要轻易修改。如果非要改,一定要做好版本管理(比如 /v1/orders, /v2/orders)。接口的命名要自解释,比如 getUserById(userId) 就比 getData(id) 好得多。
3.3.2 参数要精简
接口的参数越少越好。如果一个方法需要 5 个以上的参数,你就该考虑是不是设计有问题了。可以用一个对象来封装多个参数。
// 不好的设计:参数太多,顺序容易搞错
public boolean createOrder(String userId, String productId, int quantity, double price, String address, String couponId) {
// ...
}
// 好的设计:用对象封装参数
public boolean createOrder(CreateOrderRequest request) {
// ...
}
// 请求对象
public class CreateOrderRequest {
private String userId;
private String productId;
private int quantity;
private double price;
private String address;
private String couponId;
// getters and setters...
}
3.3.3 错误处理要规范
接口调用失败了怎么办?不要只返回一个 null 或者 -1。应该返回明确的错误码和错误信息。这样调用方才能知道到底出了什么问题,是参数错了,还是系统内部崩了。
| 错误码 | 含义 | 说明 |
|---|---|---|
| 1001 | 参数校验失败 | 调用方传入的参数不合法 |
| 2001 | 资源不存在 | 请求的数据未找到 |
| 3001 | 权限不足 | 当前用户无操作权限 |
| 5001 | 系统内部错误 | 服务器出了未知问题,需要排查 |
3.4 实践中的权衡
理论说完了,咱们聊聊现实。你可能会问:「我是不是要把系统拆得越细越好?」 嗯,这里要注意,凡事过犹不及。模块拆得太细,会导致接口数量爆炸,系统复杂度反而上升。
我个人习惯,遵循一个「适度原则」:
- 如果一个模块的代码超过 1000 行,考虑拆分。
- 如果一个模块的职责可以用一句话说清楚,那就先别拆。
- 如果两个模块之间的通信过于频繁(比如一次业务操作要调用 10 次接口),考虑合并。
说白了,系统设计不是做数学题,没有标准答案。它是一个不断权衡、不断演进的过程。你刚开始可能拆得不够好,没关系,随着对业务的理解加深,你会慢慢找到那个「刚刚好」的点。
好,这一章就到这里。下一章,咱们聊聊具体的设计工具和方法,比如 UML 图、设计模式这些实战利器。