领域驱动设计( Domain Driven Design )的概念已经被发明了十多年,而且也不乏相关著作,但是业界宣称自己应用了DDD原则的项目,软件却鲜有耳闻。随着微服务架构的流行,DDD在边界识别,服务划分等方面不断被提及,作为一种应对复杂软件的方法论,似乎又被重视起来了。
那么,为什么这个听起来很靠谱的方法论实际上很难实施呢?我们以DDD创始人 Eric Evans
的经典书籍《领域驱动设计:软件核心复杂性应对之道》为例,分析一下,可能的原因是这样的:
领域,子域,核心子域,通用子域,实体,值对象,领域服务,应用服务,领域事件,统一语言,衔接上下文,遵循者等等。DDD中有着大量的新概念,而且这些概念有些是技术相关的,有些是问题相关的,交织在一起之后,很难理清头绪。
DDD中的模式又分为战术的和战略的两大部分,有很多团队应用了战术相关的,比如实体,值对象,领域服务,仓库等模式,代码看似DDD,实则与DDD强调的 以领域为中心
相去甚远,陷入了开发者太过于关注技术本身的老路上,这种现象还有个专门的名词,叫 DDD-Lite 。
DDD要求读者既有具体Coding的技能,有需要跳出圈外,以架构师的角度来审视整个系统。DDD需要开发者以一个全新的视角去认识软件开发,强调对业务流程的熟悉,强调与领域专家一起协作,强调软件的表达能力和进化能力。而这些思维方式的转变,都有很大阻力的(想想从面向对象世界切换到函数式编程,再到响应式函数编程的切换)。
It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure. Basic professional ethics would instead require him to write a DestroyCity procedure, to which Baghdad could be given as a parameter. – Nathaniel Borenstein
我自己在工作的前4年,非常反感业务,认为 case by case
的业务流程会严重影响我的代码的通用性。然而事实是,纯粹通用的代码是不存在的。毕竟,诸如IoC容器,Web等基础设施已经相当完善,完全无需我们重复发明轮子。而作为应用开发人员,更重要的是在充分理解业务的前提下,写出易于维护,易于扩展,可以更快速响应业务变化的代码。
正因为如此,DDD在一定程度上会让程序员感到不适,它太强调领域了,而领域又是独一无二的,每个公司的每个系统都有其独立性,这就要求你的代码可能没法做到 纯粹
。
要解决上面提到的这些问题,正确的实施DDD来指导实际的开发工作,需要至少做到这样几个事情。首先,明确分清 问题
和 方案
(这是初学DDD者最容易犯的错误);其次,转变思维方式,将软件开发的重心放在梳理并明确业务需求上,而不是代码逻辑上;最后,需要应用一些合理的工程实践来促成DDD-Lite的落地。
我们说领域(子域,通用子域,支撑子域等)的时候,是在讨论问题域,即定义我们要解决的问题是什么。而说到限界上下文,聚合,实体,仓库则是在讨论解决方案部分,人们很容易将两者搞混。
在电商的领域中,一些典型的问题是:
要解决这些问题,人们可能会开发出来一个软件系统,也可能会用手工的流程,也可能是混合模式。同样,一些具体的解决方案的例子是:
显然,解决方案是在问题定义之后才产生的,而定义问题本身是一件 开发人员
不擅长的工作,这也是为什么DDD特别强调领域专家的原因。实施DDD需要从领域专家,技术专家的深入合作中,得出一个模型。其实,DDD的核心就是要解决一个问题:建立领域问题的软件模型,这个模型需要满足这样几个条件:
开发者要将思维方式转变成 以业务规则优先
是一件非常困难的事儿。毕竟经过了多年的训练,特别是抽象思维的训练,开发者很喜欢 通用
的技巧,比如管道-过滤器,线程池,解析器等等。然而业务规则往往是具体的,而且很多时候,需求变化的方向会破坏掉开发者精心构筑的抽象体系。
然而个思维方式的转变正是成功实施DDD关键所在:愿意和业务人员一起理解沟通,理解业务是实施DDD的第一步。其实每个行业的业务都有很多有意思的地方,我在ThoughtWorks,由于工作性质的原因,可以接触到很多不同的客户,不同的业务。每个项目上,我都很乐于去向业务人员学习,业务在现实世界中是如何运作的:房产中介如何打广告,房东如何付费,房地产广告平台如何从中盈利;无线基站如何建设,工人如何去施工,甚至基站铁塔如何避雷,如何防雨;保单的类型,付费年限,如何分红等等。
每个不同的业务都可以学到很多人们在解决问题时发明出来的新思路,新方法。这些思路和方法未尝不可以反过来应用在软件开发上。
其实早在敏捷宣言产生的时代,人们就已经发现了 客户合作胜过合同谈判
。与客户保持高度的合作(最好是业务人员就坐在开发人员旁边),实时的反馈收集并根据反馈进行方向调整都是敏捷方法中倡导的。而DDD更进一步,需要和业务人员一起,定义出领域模型的原型,包括一些白板上的图和代码原型,并根据敏捷方法进行持续的演进。
比如这里提出的一些实践可以比较好的帮助你来实施 DDD
:
通过敏捷实践,我们可以建立起快速的 反馈机制
,和对变化的响应能力,冗长的流程和不透明的价值流向会导致很多问题。
另一方面,要保证模型的可理解性,除了 Clean Code
的一些原则之外,自动化测试也是一个必不可少的工程实践。领域模型在 意图表现 上需要做到极致,而单元测试/功能测试则以用例的方式将模型 用起来
。这要求开发者使用 实例化需求
, 行为驱动开发
等方法编写实践作支撑。
测试不是为了覆盖率,也不能仅仅只是为了自动化一些手工的工作。在DDD的上下文中,自动化测试更多的是完整表达业务意图的用例。
测试需要以实际用户的角度来编写,并以 实例
的方式描述。这样即使自动化做不了,这个 实例
依然是一个很好的文档。当然,为了保证这个文档不过期(和代码实现不匹配),还是强烈建议可以将其自动化起来。
比如这里有个例子,人事经理想要找一些开发人员来分配到项目上,这个找人的过程有两条很简单的规则:
用 Cucumber
写的测试看起来是这样的:
Feature: Find employee by skills As a staffing manager I want to find employee by skills So that I know whether we have the staff or we need to hire new ones Background: persona Given we have the following employees: | name | currentProject | role | skills | | Juntao | Consulting | Dev | Java,Ruby | | Yanyu | Beach | Dev | Ruby | | Jiawei | Beach | Dev | Java,Ruby | | Momo | Beach | Dev | Python | Scenario: Search by skills Given I have a project which require "Ruby" as language When I search staff by skill Then I should get the following names: | Yanyu | | Jiawei |
软件开发本身就具有很高的复杂度,正如我在上一篇博客里提到的,在项目启动之初,无论是业务专家还是开发者,对软件系统的知识都是非常有限的。迭代式的开发方式更加契合这种复杂度很高的活动。业务人员和开发一起,在建模、实现的过程中会对领域进行深入的学习,并产生新的知识。反过来这些新的知识又会影响模型的进一步进化。
迭代开发的方式可以帮助我们将这个复杂度很高的过程变得平缓一些,而且从一个个小的迭代中学习,并根据反馈来指导新的方向,可以快速的帮助团队建立信心,同时对业务的理解更为深入,使得整个开发过程越来越平顺。
简而言之,要顺利实施DDD,需要至少做到:
问题域
和 解决方案域
业务梳理和理解
赋予最高的优先级 工程实践
来确保落地