问题:我对于领域模型如何表示始终还不太明白。按照Evans书里的说法,代码应当是领域模型的主要部分,文档、图表作为补充。另外一方面,领域模型应当是所有参与者都能够理解的,而我觉得用户不太可能去理解代码。
比如以Evans书里举的,可以超载10%这一点,书里是通过一个Strategy模式来表达这个知识,从程序员的角度看很清晰了,但是从用户的角度看,还是不太能够明白吧。
请教张老师如何看待这个问题?
回答:其实从模型的角度看,有几个层次,Eric说的是模型驱动模型。重点是模型。这几个层次包括:
- 现实模型即问题域
- 领域模型
- 设计模型
- 代码模型
实际上领域模型是搭建现实模型需求问题到解决方案的桥梁。领域模型是领域概念尤其是统一语言的可视化表现,在Eric写作《领域驱动设计》一书的时代,领域模型多数以UML来表达。
这里要注意一个历史问题。在Eric写作该书的时代,正是UML与逆向工程大行其道的时代。当时有很多人都在倡导运用建模工具如Rational Rose来建模,进而利用图形化的模型生成代码。这个思想在当时人们的心中会是未来编程的一个主流发展方向,也有很多人在朝着这个方向努力,随之也催生了诸多建模工具的诞生,UML得到大量的普及,甚至差点成为了软件设计的唯一标准。这也是Eric倡导模型驱动设计的一个历史背景,至少我认为他在写书时是收到这个思想影响的。最终,这种设计思想并没有得以实现,人们低估了编程的复杂度,高估了模型的重要性。所以,Eric的书是有历史局限性的。尽信书不如无书,这是阅读他的书要注意的。书中讲的一些实践,未必都对。
但是,Eric的领域驱动设计是一个方法学,是开放的,也是逐步演进的。事实上,已经有很多人站在Eric的肩膀上,提出了很多切合实际,也吻合软件行业发展趋势的实践与模式,作为领域驱动设计的补充。例如领域事件、六边形架构以及CQRS等。Eric自己也认可这种演进。
回到模型上来。我认为领域模型就是对领域概念的抽象,你说的超载10%其实就是业务规则,所以可以抽象为一个领域概念,在与领域专家进行交流时,可以通过领域模型的这个领域概念来表达,而不是直接使用代码。
设计模型则是对领域模型的一种技术呈现,乃至于是从技术角度的一种精化与演进,例如通过引入设计原则与模式,可以实现领域模型对象更好的职责分配,通过抽象实现解耦,定义更加合理的封装。这时,设计模型要取决于你的编程范式,如采用面向对象还是函数式编程。同样以超载规则为例,面向对象范式的设计模型就是抽象的服务接口,函数式就是一个函数。如果规则需要组合,前者就利用继承或委派,后者就用组合子。
代码模型是设计模型的具体实现,它是遵循设计模型来实现的,采用不同的语言和框架,也会有区别。例如,有的语言可以非常方便地定义值对象,如Scala的Case Class,就是值对象的语法糖。
整体来看,领域模型是团队与领域专家交流所用,设计模型是团队的设计人员交流的工具,代码模型自然为程序员服务。这三个模型之间的关系如下图所示:
随着时间的推移,这三种模型可能会出现不同步的问题。Eric在书中讲解模型驱动设计时也提到了这个问题。如上图所示,领域模型为指导设计模型,设计模型是领域模型的实现,而随着设计模型的演进,我们又需要这种变更体现在领域模型中,保证模型是领域的真实表达。至于代码模型,一方面是遵循设计模型进行代码的实现,同时还应该尽力保障代码模型要表达领域概念,这不仅仅是从代码可读性的角度来考虑,也牵涉到代码对领域逻辑的呈现。这也是为什么在DDD的编程实践中,我们为什么希望避免贫血模型,希望避免使用无法表达领域行为的get和set方法的原因。
倘若要在代码模型中体现领域模型,一种更好的做法是使用DSL,即领域特定语言。但DSL的实现其实是一个相对漫长的积累过程,不同语言的领域表达能力也不相同。所以DSL主要还是用在一些相对复杂但又相对稳定专业的行业中,例如通信和金融行业,就有DSL的开发需求。当然,即使不去做一套DSL,我们也可以借鉴DSL的思想,例如通过Fluent Interface之类的实践改进代码的表达能力。
还有一种做法就是利用BDD编写验收测试,形成活文档(Live Document)。BDD框架如Cucumber、Robot Framework、RSpec其实就是一种DSL,通过这些框架可以编写符合自然语言规范的测试用例,形成一个中规格(Specification),这些测试用例又是能够运行的代码,这就相当于搭建了代码与需求规格的桥梁。不过,这种活文档只能应用在测试保障上,它可以帮助我们建立一种更好的交流机制,但并不能取代设计模型和代码模型。