前文回顾
上一篇介绍了该书的第三部分“通过重构来加深理解”,我们学习了SPECIFICATION模式,同时分析了一个应用该模式的案例。
这一篇,我们继续学习该书的第三部分。
软件的目的
软件的最终目的是为用户服务,但它首先必须为开发人员服务。在强调重构的软件开发过程中尤其如此。 软件的设计必须要让开发人员乐于使用,而且易于做出修改,这就是柔性设计(supple design)。
很多软件的设计有着过多的抽象层和间接设计,常常成为项目的绊脚石,但这样的过度设计(over engineering)却借着灵活性的名义而得到合理的外衣。看一下真正为用户带来强大功能的软件设计,你常常会发现一些简单的东西。注意:简单往往很难做到。
开发人员通常都扮演了两个角色:一个角色是客户开发人员,一个是代码修改人员。客户开发负责将领域对象组织成应用程序代码。柔性设计能够揭示深层次的底层模型,让客户开发人员可以灵活地使用一个最小化的、松散耦合的概念集合,并用这些概念来表示领域中的众多场景。柔性设计也可以帮助代码修改人员,把客户开发人员正在使用的同一个底层模型表示出来,便于代码修改,让设计更加易于理解。
设计这样的软件并没有公式,作者Eric精选了一组模式,合理的运用这些模式,就有可能获得柔性设计。
模式:INTENTION REVEALING INTERFACES
如果开发人员为了使用一个组件而必须要去研究它的实现,那么就失去了封装的价值。
释意接口(INTENTION REVEALING INTERFACES)指的是,在给类和它的操作命名时,要描述它们的效果和目的,而不要表露它们是通过何种方式实现的。这样可以使客户开发人员不必去理解内部细节。这些名称应该与UBIQUITOUS LANGUAGE保持一致,以便团队成员可以迅速推断出它们的意义。可以在创建一个行为之前先为它编写一个测试,这样可以促使你站在客户开发人员的角度上来思考它。
上图是调漆程序中的一个类,paint
这个方法从名字上看不出它的作用,看代码才知道是把两种油漆(Paint)混合到一起。另外v,r,g,b
也让人琢磨不透。按照INTENTION REVEALING INTERFACES模式重构之后,读者就更容易明白它的意图了。
模式:SIDE-EFFECT-FREE FUNCTION
“副作用”这个词暗示着“意外的结果”。在软件系统中,一个操作可能会调用另一个操作,在多层嵌套的调用中,第二层或更深层的操作可能并不是开发人员有意为之。于是它们就变成了副作用。
只返回结果而不产生副作用的操作称为函数。
我们可以宽泛地把操作分为两个大的类别:命令(Command)和查询(Query)。查询是从系统获取信息,查询的方式可能只是简单地访问变量中的数据,也可能是用这些数据执行计算。命令(也称为修改器Mutation)是修改系统的操作。
有两种方法可以减少命令产生的问题。首先,可以把命令和查询严格地放在不同的操作中。确保导致状态改变的方法不返回领域数据,并尽可能保持简单。在不引起任何可观测到的副作用的方法中执行所有查询和计算。第二,总是有一些替代的模型和设计,它们不要求对现有对象做任何修改。相反,它们创建并返回一个VALUE OBJECT,用于表示计算结果。
尽可能把程序的逻辑放到函数中,因为函数是只返回结果而不产生明显副作用的操作。严格地把命令(引起明显的状态改变的方法)隔离到不返回领域信息的、非常简单的操作中。当发现了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂逻辑移到VALUE OBJECT中,这样可以进一步控制副作用。
如上所示,mixIn()方法中发生了很多事情,但这个设计确实遵循了“修改和查询分离”这条原则。这里并没有对Paint2进行修改。
模式:ASSERTION
使用ASSERTION(断言)可以把副作用明确地表示出来,使它们更易于处理。
如果操作的副作用仅仅是由它们的实现隐式定义的,那么在一个具有大量相互调用关系的系统中,起因和结果会变得一团糟。理解程序的唯一方式就是沿着分支路径来跟踪程序的执行,这导致封装完全失去了价值。
因此使用ASSERTION,把操作的后置条件和类及AGGREGATE的固定规则表述清楚。如果在你的编程语言中不能直接编写ASSERTION,那么就把它们编写成自动的单元测试,或者把它们写到文档中。
模式:CONCEPTUAL CONTOUR
人们有时候会对功能进行更细的分解,以便灵活地组合它们,有时却要把功能合成大块,以便封装复杂性。从单个方法的设计,到类和MODULE的设计,再到大型结构的设计,高内聚低耦合这一对基本原则都起着重要的作用。这两条原则既适用于代码,也适用于更高层次的概念。
把设计元素(操作、接口、类和AGGREGATE)分解为内聚的单元,在这个过程中,对领域中一切重要划分的认识理解也逐渐加深。在连续的重构过程中观察发生变化和保证稳定的规律性,并寻找能够解释这些变化模式的底层CONCEPTUAL CONTOUR(概念轮廓)。使模型与领域中那些一致的方面相匹配,也正是这些一致的方面使得领域成为一个有用的知识体系。
寻找在概念上有意义的功能单元,可以使得设计既灵活又易懂,通过反复重构最终会实现柔性设计。随着代码不断适应新理解的概念或需求,CONCEPTUAL CONTOUR也就逐渐形成了。
模式:STAND-ALONE CLASS
当模型中的互相依赖过多时,我们就必须把大量问题放在一起考虑,我们可能会遇到“概念过载”(conceptual overload)的问题。
MODULE和AGGREGATE的目的都是为了限制互相依赖的关系网。当我们识别出一个高度内聚的子领域并把它提取到一个MODULE中的时候,一组对象也随之与系统的其他部分解除了联系,这样就把互相联系的概念的数量控制在一个有限的范围之内。
低耦合是对象设计的一个基本要素,要尽一切可能保持低耦合,把其他所有无关概念提取到对象之外。这样的类就变得完全独立了,这就使得我们可以单独地研究和理解它。每个这样的独立类都极大地减轻了因理解MODULE而带来的负担。低耦合是减少概念过载的最基本办法,STAND-ALONE CLASS(独立的类)是低耦合的极致。
我们的目标不是消除所有依赖,而是消除所有不重要的依赖。 当无法消除所有的依赖关系时,每清除一个依赖对开发人员而言都是一种解脱,使他们能够集中精力处理剩下的概念依赖关系。尽力把最复杂的计算提取到独立的类中,实现此目的的一种方法是从存在大量依赖的类中将VALUE OBJECT建模出来。
以调漆程序为例,从根本上讲,油漆的概念与颜色的概念紧密相关。但在考虑颜色(甚至是颜料)的时候却与不必去考虑油漆。通过把这两个概念变为显式概念并精炼它们的关系,所得到的单向关联就可以表达出重要的信息。
通过将颜色数据和相关操作移出来,Paint变简单了,而且我们得到了一个独立的类PigmentColor。
模式:CLOSURE OF OPERATION
两个实数相乘,结果仍为实数(实数是所有有理数和所有无理数的集合)。由于这一点永远成立,因此我们说实数的“乘法运算是闭合的”:乘法运算的结果永远无法脱离实数这个集合。当我们对集合中的任意两个元素组合时,结果仍在这个集合中,这就叫做闭合操作。
大部分引起我们兴趣的对象所产生的行为仅用基本类型是无法描述的。一种对设计进行精化的常见方法就是我所说的CLOSURE OF OPERATION(闭合操作)。这个名字来源于最精炼的概念体系,即数学。
在适当的情况下,在定义操作时让它的返回类型与其参数的类型相同。如果实现者(implementer)的状态在计算中会被用到,那么实现者实际上就是操作的一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。大部分闭合操作都应该到VALUE OBJECT中去寻找。
在前面的示例中,PigmentColor的mixedWith()操作是PigmentColor之下的闭合操作。
声明式设计
声明式设计通常是指一种编程方式:把程序或程序的一部分写成一种可执行的规格(specification)。使用声明式设计时,软件实际上是由一些非常精确的属性描述来控制的。
笔者经历过的项目中,曾经利用OpenAPI Specifiaction描述API接口规范,并且使用OpenApiGenerator生成相关代码,这就一个声明式编程的典型案例。
这种设计也存在不足,声明式语言并不足以表达一切所需的东西,它把软件束缚在一个由自动部分构成的框架之内,使软件很难扩展到这个框架之外。代码生成技术破坏了迭代循环。
基于规则的编程(带有推理引擎和规则库)是另一种有望实现的声明式设计方法。笔者也曾经开发过一种称为决策表的规则,并使用Drools规则引擎执行这些写在Excel文件中的规则。
声明式设计发挥的最大价值是用一个应用范围非常窄的框架来自动处理设计中某个特别单调且易出错的方面,如持久化和对象关系映射。最好的声明式设计能够使开发人员不必去做那些单调乏味的工作,同时又完全不限制他们的设计自由。
一旦你的设计中有了INTENTION REVEALING INTERFACE、SIDE-EFFECT-FREE FUNCTION和ASSERTION,那么你就具备了使用声明式设计的条件。当我们有了可以组合在一起来表达意义的元素,并且使其作用具体化或明朗化,甚或是完全没有明显的副作用,我们就可以获得声明式设计的很多益处。
总结
- INTENTION REVEALING INTERFACE使客户能够把对象表示为有意义的单元,而不仅仅是一些机制。
- SIDE-EFFECT-FREE FUNCTION和ASSERTION使我们可以安全地使用这些单元,并对它们进行复杂的组合。
- CONCEPTUAL CONTOUR的出现使模型的各个部分变得更稳定,也使得这些单元更直观,更易于使用和组合。
- STAND-ALONE CLASS可以简化模型依赖关系,减少概念过载问题。
- CLOSURE OF OPERATION可以对设计进行精炼,减少对外部概念的依赖。
本文展示了一系列技术,它们用于澄清代码意图,使得使用代码的影响变得显而易见,并且解除模型元素的耦合。
系列文章
- [DDD读书笔记] 运用模型①什么是领域模型
- [DDD读书笔记] 运用模型②通用语言
- [DDD读书笔记] 构造块①分离领域层
- [DDD读书笔记] 构造块②实体、值对象和服务
- [DDD读书笔记] 构造块③模块
- [DDD读书笔记] 构造块④聚合与工厂
- [DDD读书笔记] 构造块⑤仓库
- [DDD读书笔记] 构造块⑥实战模拟
- [DDD读书笔记] 重构①突破
- [DDD读书笔记] 重构②SPECIFICATION模式
- [DDD读书笔记] 重构③柔性设计
- [DDD读书笔记] 重构④使用分析模式和设计模式建模
- [DDD读书笔记] 战略设计①模型上下文策略
- [DDD读书笔记] 战略设计②精炼
- [DDD读书笔记] 战略设计③大型结构