本章的核心观点是:设计不仅要为用户服务,更要为开发人员服务。一个“柔性”的设计易于理解、修改和组合,能加速开发进程,而不是随着系统老化而停滞不前。 它是深层建模的补充,通过一系列模式,将模型打造成开发人员乐于使用和修改的工具。
核心梳理
1. 柔性本质
柔性设计的目标是使代码表现出意图(易于理解)、产生可预测的结果(易于使用)且易于修改和重组(易于维护)。它不是过度设计的产物,而是通过持续重构,使设计与领域深层概念轮廓高度吻合的结果。
2. 关键模式与原则
INTENTION-REVEALING INTERFACES(释意接口): 通过命名清晰地表达操作的意图和效果,而不是实现方式。不要命名一个方法 "mix(Paint paint)",而要命名 "blend(Paint paint) ",让使用者立刻明白这是“混合”而不是“加入”或“搅动”。类名、方法名、参数名共同构成了释意接口。- SIDE-EFFECT-FREE FUNCTION(无副作用函数): 将复杂逻辑封装在不改变任何状态的函数中。它只返回结果,不产生可见的副作用。这类函数是“安全的”,可预测的,易于测试和组合,也便于并发执行。VALUE OBJECT是实现这种函数的天然工具,因为它是不可变的。
- ASSERTION(断言):明确声明操作的后置条件(副作用产生的效果)和类/聚合的固定规则。使开发者无需阅读方法内部实现就能预测执行结果。断言的“契约”让封装和抽象变得安全。如果语言不支持,则用自动单元测试替代。
CONCEPTUAL CONTOUR(概念轮廓): 将设计元素(类、方法、模块)分解为与领域中的概念轮廓一致的内聚单元。寻找那些“高内聚、低耦合”的概念分组。使模型与领域深层结构高度吻合。当模型需要修改时,只需要影响与某个特定概念轮廓相关的部分,而非四处开火。STANDALONE CLASS(独立类): 尽可能减少类的依赖。目标是创建一个只依赖基本类型(如int、String)或极少数标准库类的类。它极大地减轻了理解负担,使开发者能单独研究和测试它,从而集中精力解决其核心计算逻辑的复杂性。- CLOSURE OF OPERATION(操作闭合):定义一个操作,使其返回值类型与参数类型相同。这样可以将操作串联起来形成管道。"SharePie.add(SharePie other )" 返回一个新的 "SharePie",使你能够写出 "pie1.add(pie2).subtract(pie3 ) " 这样的声明式代码。这简化了客户端代码。
3.声明式设计风格
当代码具备了"INTENTION-REVEALING INTERFACE"、"SIDE-EFFECT-FREE FUNCTION"和"ASSERTION"时,就可以自然地使用声明式风格编写代码。在这种风格的代码中,你不再描述“如何”一步步实现,而是描述“要什么”。例如,在“股份数学”的例子中,"loan.payPrincipal (amount, sharePie)" 这样的代码就比一系列复杂的计算语句更接近声明式。
项目使用时的指导原则
基于以上核心思想,在项目中应用“柔性设计”时,可以遵循以下原则:
1. 将“读”代码的能力作为设计的第一标准 🤝
原则:你的代码平均被阅读的次数远超被编写的次数。可读性是最重要的质量指标。一个“聪明”但难懂的实现,不是柔性设计;一个简单、清晰、即使新手也能快速理解的实现,才是。
行动:
命名即文档:花足够的时间为类、方法、参数命名。好的命名应揭示意图("calculateOverdueFee()" 优于 "calcFee()")。当你想不出一个好名字时,这通常意味着你的模型或设计还不清晰。
编写最小化注释:好的代码本身就能说明自己。注释应解释“为什么”要这样做(业务或设计原因),而不是“如何”做(这是代码的责任)。
代码评审聚焦“可读性”:在评审中,除了寻找Bug,更要看:一个新来的开发人员能否在5分钟内理解这个类的核心职责?方法的意图是否一目了然?
2. 积极识别并重构出“无副作用函数”和“独立类” 🧩
原则:复杂逻辑是Bug的温床。主动将复杂计算逻辑从实体和方法中提取出来,封装到无副作用的独立值对象函数中。
行动:
提取计算逻辑:当你看到一个方法既修改状态又执行复杂计算时,将其拆分为:一个SIDE-EFFECT-FREE FUNCTION返回计算结果;另一个简单的命令来应用结果。例如,"Order.calculateDiscount()" 返回折扣金额, "Order.applyDiscount (orderId)" 应用它。
追求“独立”:识别模型中那些只依赖基本类型和标准库的纯计算逻辑(如金融中的“股份数学”、日期计算、费率计算)。将它们提取为STANDALONE CLASS。为这个类编写详尽的单元测试,你将获得高度可靠且可重用的业务逻辑。
3. 用“ASSERTION”或“测试”来明确契约 📜
原则:接口只能说明“能做什么”,ASSERTION或测试才能说明“会产生什么结果”。它们是编码团队之间的正式契约。
行动:
为每个方法定义“后置条件”:在代码注释或官方文档中,明确说明调用一个方法后一定会发生什么。例如,"// after this method, the sum of all LineItems == Order.total"。
使用单元测试作为断言:如果语言不支持形式化的ASSERTION,强大的单元测试就是最佳替代品。测试应验证方法的“后置条件”和类的“固定规则”。在重构时,这些测试就是你的安全网。
4. 追求“声明式”而非“命令式”的领域代码 ✍️
原则:领域代码应该像业务需求文档一样,读起来像是“是什么”,而不是“怎么做”。这是柔性设计的终极体现。
行动:
识别可组合的模式:寻找类似“股份数学”这样的模式——即存在一组操作("add"、"subtract"、"multiply")可以按任意顺序组合出不同业务场景。为这类模式应用CLOSURE OF OPERATION和SIDE-EFFECT-FREE FUNCTION,就能写出高度声明式的客户端代码。
警惕过程式服务:如果一个领域服务的方法体充满了循环和if-else(如 "for(...){ if (...){ ... } }"),这通常是命令式风格,而非声明式。尝试将这种行为抽象为更明确的领域对象(如 "Specification"、"Policy"、"Calculator"),使服务代码变成“调用这些对象”的简单声明。
5. 从“最重要”的部分开始,逐步实现柔性设计 🎯
原则:不要试图一次性让整个系统都变得“柔性”。这是不可能的。选择最具复杂性、最具业务价值、最需要变化的部分(通常是CORE DOMAIN)开始攻坚。
行动:
识别目标:使用第15章的“精炼”原则,找到CORE DOMAIN或最复杂的GENERIC SUBDOMAIN。
分步改进:先应用"INTENTION-REVEALING INTERFACES"让接口意图清晰,然后使用"SIDE-EFFECT-FREE FUNCTION"隔离复杂计算,再用"CONCEPTUAL CONTOUR"重构对象职责,最后看看是否能引入声明式风格。
控制范围:不要在修改时尝试一次性应用本章所有模式。选择一个模式,进行小范围重构,验证效果,然后继续下一个。
总结: 在项目中运用“柔性设计”的理念,核心是为开发人员降低认知负担,使他们能够快速、安全地理解和修改代码。其指导原则是:将可读性作为设计标准;积极提取计算逻辑至无副作用的独立类;用断言或测试明确契约;追求声明式而非命令式风格;并从业务核心区域开始循序渐进地实现。
夜雨聆风