把复杂性关在模块内部,把稳定性暴露给外部。
这是从“会写功能”走向“会设计程序”的关键分界线。
很多程序员在成长早期,最关心的是功能能不能跑通:按钮点了有没有反应,接口调了有没有数据,页面有没有按预期展示。这当然重要。功能跑不通,一切都谈不上。
但当系统开始变大,真正拉开差距的就不再只是“能不能写出来”,而是“写出来之后能不能被理解、被测试、被修改、被长期维护”。
这时候,问题会变成另一种:
调用方看到的是稳定含义,还是实现细节?
如果调用方看到的是稳定含义,模块就更容易协作和演进。
如果调用方依赖的是实现细节,短期可能很快,长期一定越来越难改。
功能思维:先把事情做出来
“会写功能”的典型思路是直接解决眼前问题。
页面要展示数据,就在页面里处理数据。
接口有重复返回,就在调用处去重。
顺序不对,就在展示前排序。
状态缺了,就在组件里补一个变量。
这种写法在小需求里很自然,也经常是必要的。它追求的是快速闭环:输入进来,逻辑处理,结果展示。
问题在于,当异常场景越来越多,逻辑会慢慢散落到每一个调用方里。页面知道太多,组件知道太多,测试也知道太多。每个地方都懂一点内部细节,每个地方都补一点边界情况。
最后系统不是不能运行,而是不能轻松变化。
你改一个内部规则,发现页面要改。
你换一种数据结构,发现测试要改。
你新增一种异常场景,发现调试面板、展示层、核心逻辑都要一起改。
这就是复杂性没有被收住的结果。
设计思维:先决定复杂性应该待在哪里
“会设计程序”的人,不是不会写功能,而是在写功能之前,会多问一个问题:
这份复杂性应该由谁承担?
有些复杂性属于模块内部。比如缓存怎么维护、重复数据怎么识别、乱序数据怎么整理、临时状态怎么推进。这些东西很重要,但它们不应该被每一个调用方理解。
有些稳定性应该暴露给外部。比如当前可展示的结果是什么、当前状态是否完成、哪些数据还在等待、失败原因是什么、下一步允许做什么。这些是调用方真正需要依赖的含义。
设计的关键,不是消灭复杂性,而是给复杂性找到正确的位置。
复杂性可以存在,但要被关在模块内部。
外部可以使用,但只能依赖稳定合约。
这就是模块边界。
为什么边界会决定系统能不能演进
一个模块如果没有边界,调用方就会忍不住直接伸手进去拿东西。
今天页面直接读取内部缓存,因为这样最快。
明天测试直接断言内部字段,因为这样最好写。
后天另一个组件直接调用内部方法,因为这样最省事。
每一次看起来都只是一个小捷径,但这些捷径会把内部实现变成外部依赖。
一旦外部开始依赖内部实现,模块就失去了自由。
你不能轻松重构,因为调用方会坏。
你不能轻松替换数据结构,因为测试会坏。
你不能轻松调整流程,因为展示层也跟着坏。
这就是很多项目越写越脆的根源:不是代码不够聪明,而是边界不够清楚。
好的边界会给内部留下演进空间。只要对外含义不变,内部可以换实现、换结构、换算法、换组织方式。调用方不需要知道这些变化,也不应该被这些变化打扰。
稳定合约不是字段列表,而是语义承诺
很多时候,我们会把“接口”理解成几个字段,把“类型”理解成一段声明,把“文档”理解成补充说明。
但真正的合约不是字段本身,而是字段背后的稳定含义。
比如一个流式消息模块,内部可能要处理重复、乱序、缺口、缓存、推进游标等细节。调用方真正需要的,不是内部怎么存,也不是内部怎么循环,而是稳定的读模型:
- 当前可以安全展示的内容。
- 已经确认顺序的数据。
- 仍在等待补齐的数据。
- 当前是否出现重复或缺口。
- 下一步状态应该如何理解。
这些才是合约。
合约的意思是:你可以依赖这些含义,我保证内部变化时尽量不破坏它们。
所以,合约不是给代码加一层漂亮包装,也不是把内部字段换个名字返回出去。合约是一种取舍:什么应该被看见,什么必须被隐藏。
能做出这种取舍,才开始进入设计程序的层次。
测试也应该依赖合约,而不是绑死实现
边界不仅影响业务代码,也影响测试。
如果测试一直围绕内部字段写,它保护的就是今天的实现形状。内部一改,测试就碎,即使外部行为完全正确。
如果测试围绕合约写,它保护的是业务语义。比如乱序数据最终能否按顺序展示,重复数据是否被识别,缺口补齐后状态是否恢复正常。
这样的测试更有价值。
它不会阻止你重构内部实现,但会提醒你有没有破坏对外承诺。
这也是“会写功能”和“会设计程序”的差别之一:前者经常把测试当作验证当前代码的工具,后者会把测试当作保护稳定合约的工具。
判断能力来自反复追问
以后读一个模块,或者准备写一个模块时,可以先问几组问题:
- 这个模块接收什么输入?
- 它内部必须承担哪些复杂性?
- 它应该对外暴露哪些稳定含义?
- 哪些字段或方法只是当前实现,不应该让调用方依赖?
- 如果内部实现换掉,调用方应该保持不变吗?
- 如果需求新增一个状态,应该扩展合约,还是泄漏内部细节?
这些问题看起来简单,但它们会逼你从“怎么把功能写完”切换到“怎么让系统长期可改”。
这就是架构能力的日常训练。
架构不是只有微服务、领域模型、事件驱动这些大词。架构也存在于一个普通模块里,存在于一次字段暴露的选择里,存在于一次测试断言的边界里。
真正的成长标志
当你只会写功能时,你会关心:
这个需求怎么最快完成?
当你开始设计程序时,你会继续关心完成,但还会追问:
这份复杂性应该放在哪里?
调用方应该依赖什么?
未来变化时,哪些地方不应该被影响?
测试保护的是实现,还是语义?
这些问题没有那么热闹,却非常重要。它们决定了代码是只能解决今天的问题,还是能承接明天的变化。
所以,“把复杂性关在模块内部,把稳定性暴露给外部”不是一句漂亮的架构口号。
它是一条具体的工程判断标准。
当你开始用这条标准写代码、读代码、改代码、评审代码,你就不再只是把功能做出来,而是在训练自己设计程序。