谈谈代码中“精密的设计”
前几天修复了一个历史遗留 Bug,和同事讨论时他提到了一个词——“精密的设计”。
这个词几乎瞬间击中了我,因为它与我过去坚持的设计理念高度契合,我曾将其称为“设计正确”。 但过往经验也证明,这类“设计正确”反而制造过不少 Bug,彼时我将问题归咎于“抽象不完善”。
支持这一理念的典型论据是:人体就是精密的。作为万物之灵,我们理应从自然中汲取灵感。然而重新审视这个问题时,我发现过去的理解过于片面了。
疫情期间学到的医学常识给了我新视角:人体并非绝对精密,而是充满冗余和容错机制。例如少量病毒入侵未必致病,免疫系统会自主应对。
同理,若必须依赖“完美抽象”才能避免 Bug,这样的“精密设计”真的是理想方案吗?
《程序员修炼之道(第二版)》中反复强调的核心观点是:代码应“易于修改”。
无论是设计模式还是编程准则,终极目标都是应对需求变化。无法灵活调整的设计,终将沦为技术债务。
而我曾经的“设计正确”理念,本质是落入了契约式编程(Design by Contract)的陷阱。
当时的逻辑是:A 模块调用 B 模块时已确保参数合法,因此 B 无需验证输入。
尽管我早年就意识到“设计闭环”的重要性,但在实际编码时,“性能强迫症”仍驱使我走向极端。
近年来随着对性能执念的淡化,我开始反思:这种追求“设计正确”的代码真的易于修改吗?答案显然是否定的。
精密代码如同多米诺骨牌阵列,看似严谨,却可能因一处微小改动引发系统性崩溃。过去踩过的坑已充分印证了这一点。
从“高内聚,低耦合”角度来重新审视这个问题。
若 B 模块要求调用方保证参数合法性,本质上已经形成了隐性耦合。用设计原则来描述,这违背了最少知识原则(Law of Demeter)。
根据多年经验,实现“高内聚,低耦合”的关键只有一条:
设计模块(函数/类/服务)时,永远不要假设调用方会遵守任何约定。
无论外部如何调用,模块内部必须维持自身数据一致性。
这既是我曾提过的“设计闭环”,也可称为防御式编程或容错冗余。
原则看似简单,实践却困难重重。
一个典型的开发场景是:先实现调用方 A,再开发被调用方 B。
当编写 B 时,由于已知 A 的实现细节,开发者会不自觉地利用这些信息优化 B 的性能。
这恰恰是坏的味道的开始——尽管短期运行高效,却埋下长期隐患。
假设需求变更后,需要用新模块 C 替换 A:
由于 C 与 A 的相似性,开发者容易忽略细微差异。若 B 的实现依赖了 A 的某些隐性特征,便可能催生极难发现的边界 Bug。
更棘手的难题在于:容错冗余的粒度如何把握?函数级、类级、模块级还是服务级?这需要开发者基于场景反复权衡,而每个人心中的天平刻度未必相同。
我自己现在的准则是:
- 模块/类级/服务级:必须严格校验输入边界和状态约束,防止预期之外的调用弄乱内部数据。
- 函数级:可根据情况相信传入的数据,这也有利于我们放心大胆的对于函数进行抽象,而不必担心性能的销耗。
ps. 函数的抽象绝不仅仅是代码复用,甚至可以说根本不是为了代码复用——抽象的本质是建立认知边界。只有深刻理解这一点,才有可能做出更好的抽象决策。