《实现领域驱动设计》笔记——DDD入门
设计不只是感观,设计就是产品的工作方式。
我们的目标应该是创造一个可观测的、可伸缩的、组织良好的软件模型。
DDD同时提供了战略上的战术上的建模工具。
我能DDD吗?
DDD首先并不是关于技术的,而是关于讨论、聆听、理解、发现和业务价值的,而这些都是为了将知识集中起来。如果你了解公司的业务,那么你至少可以为DDD的通用语言做出贡献。当然,你可能需要学习更多的业务知识。
领域专家并不是一个职位,他可以是精通业务的任何人。
并不是说技术不重要,而是说需要掌握领域建模中更高层次的概念。
领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现,这些对象包含了数据和行为,并且表达了准确的业务含义。(对领域模型的详细介绍可以参考运用领域模型)
为什么我们需要DDD
- 使领域专家和开发者在一起工作,这样开发出来的软件能够准确地传达出业务规则。当然,对于领域专家和开发者来说,这并不是单单地包容对方,而是将他们组成一个密切协作的团队。
- “准确传达业务规则”的意思是说,此时的软件就像如果领域专家是编码人员时所开发出来的一样。
- 可以帮助业务人员自我提高。
- 关键在于对知识的集中,因为这样可以确保软件知识并不只是掌握在少数人手中。
- 在领域专家、开发者和软件本身之间不存在“翻译”,意思是当大家都是用相同的语言进行交流时,每人都能听懂他人所说。
- 设计就是代码,代码就是设计。设计是关于软件如何工作的,最好的编码设计来自于多次试验,这得益于敏捷的发现过程。
- DDD同时提供了战略设计和战术设计两种方式。战略设计帮助我们理解哪些投入时最重要的;哪些既有软件资产是可以重新拿来使用的;哪些人应该被加入到团队中?战术设计帮助我们创建DDD模型中的各种部件。
难以捉摸的业务价值
开发能够传递真正业务价值的软件和开发普通的软件是不同的。具有真正业务价值的软件能够很好符合业务战略,并且可以将竞争优势融合到解决方案中。此时的软件并不是关于技术,而是关于业务。
业务知识从来就没有被集中过。开发团队必须在多方之间权衡各种需求,并确定其中的优先级。同时,团队成员的技能也是良莠不齐的。在获得所有的信息之后,团队所面临的问题在于:如何确定某种需求确实能够传递真正的业务价值?还有,我们如何去发现并暴露出这些业务价值,如何安排它们之间的优先级,并且如何实现它们?
在开发过程中,最大的鸿沟之一便存在于领域专家和开发之间。
另一个问题发生在当多个领域专家之间存在分歧的时候。
更糟糕的是,软件的技术实现可能错误地改变软件的业务规则。比如,ERP软件通常需要修改业务操作以满足某个特定用户的需求,因此ERP的成本不能单以使用许可和维护费用来计算,对业务规则的修改所产生的成本远远大于前两者。还有,技术上的翻译和解释是没有必要的,并且在使用适当开发方案的情况下是可以避免的。解决方案才是主要的投入。
DDD如何帮助我们
DDD作为一种软件开发方法,它主要关注一下三个方面:
- DDD将领域专家和开发人员聚集到一起,这样所开发的软件能够反映出领域专家的思维模型。这并不意味着我们将精力都花在了对“真实世界”的建模上,而是交付最具业务价值的软件。有时在实用和理想之间存在冲突,根据它们的互异程度,在DDD中我们将选择实用性。
- DDD关注业务战略。虽说战略设计自然地包含了战术设计,但是战略设计关注更多的则是业务的战略方向。它帮助我们定义不同团队之间的组织关系,并在这些关系有可能导致项目失败的时候提供早期预警。DDD的战略设计用于清楚地界分不同的系统和业务关注点,这样可以保护每个业务层面的服务。更进一步,这将指引我们如何实现面向服务架构或者业务驱动架构。
- 通过使用战术设计建模工具,DDD满足了软件真正的技术需求。这些战术设计工具使开发人员能够按照领域专家的思维模型开发软件。同时,所开发出来的软件是可测试的,能够尽量避免错误,能执行服务层面协议,具有很好的伸缩性,并且允许分布式计算。DDD最佳实践同时包含了高层的架构性实践和底层设计实践,关注业务规则和数据不变性,并且可以对业务规则起到保护作用。
处理领域复杂性
在使用DDD时,我们首先希望将它应用在最重要的业务场景下。对于那些可以轻易替换的软件来说,你是不会有所投入的。相反,值得你投入的是那些重要的、复杂的东西,因为这些东西将为你带来可观的回报。正因如此,我们将这样的模型命名为核心域,而那些相对次要的称为支撑子域。那么,“复杂”到底是什么意思?
不同的业务领域对于复杂的定义是不一样的。另外,不同的公司所面临的挑战不一样;成熟度不一样;软件开发能力也不一样。因此,与其去定义什么是复杂的,还不如去定义什么是重要的。这时,你的团队和管理层应该做出决定:你们开发的软件系统是否值得做出DDD投入。
DDD的作用是简化,而不是复杂化。在使用DDD时,我们应该采用最简单的方式对复杂领域进行建模,而不是使问题变得更加复杂。
如何DDD
让我们暂时撇开关于实现细节的讨论,现在来看看DDD最具威力的特性之一:通用语言。通用语言和限界上下文同时构成了DDD的两大支柱,并且它们是相辅相成的。
就现在来说,可以将限界上下文看成是整个应用程序之内的一个概念性边界。这个边界之内的每种领域术语、词组或句子——也即通用语言,都有确定的上下文含义。在边界之外,这些术语可能表示不同的意思。
领域专家对通用语言有很大的影响,因为他们最了解业务,并且深受工业标准的影响。但是,通用语言更多地是关于业务本身如何思考和运作的。当领域专家和开发者一起创建领域模型的时候,他们有时候会达成一致,有时会做一些妥协,但最终的目的都是为了创造最适合项目的通用语言。然而,最初的一致并不表示始终一致,就像其他语言一样,通用语言也会随着时间推移而不断演化改变。
如何掌握通用语言?
- 同时绘制物理模型图和概念模型,并标以名字和行为。瑞然这些图不是正式的设计图,但它们却包含了软件建模的某些方面。即使你的团队使用统一建模语言(UML)来完成建模,也不要得意忘形,因为这样可能反而不利于团队的讨论,最终将阻碍通用语言的产生。
- 创建一个包含简单定义的术语表。将你能想到的术语全部都罗列出来,包括好的和不好的,并注明好与不好的原因。
- 如果你不喜欢术语表,可以采用其他类型的文档,但记得将那些“不正式”的模型图也包含进去。同样,这里最终的目的也是发现通用语言中的术语和词组。
- 由于团队中有些人工作在术语表上,还有些人工作在文档上,此时你需要找到团队的其他人员来检查你的成果。分歧肯定是有的,你应该对此有所准备。
使用DDD的业务价值
1、获得一个非常有用的领域模型
DDD强调将精力花在对业务最有价值的东西上。我们并不过度建模,而是关注业务的核心域。有些模型是用来支撑核心域的,它们同样重要。但是,这些起支撑作用的模型在优先级上没有核心域高。
当我们将关注点放在自己的业务和别人业务的区别上时,我们便能更好地理解自己的任务所在,同时我们将更具竞争优势。
2、你的业务得到了更准确的定义和理解
随着业务模型的不断改善,人们对业务的理解也将更加深刻。在团队讨论的过程中,一些业务细节被不断地暴露出来,这些细节有助于掌握业务价值。
3、领域专家可以为软件设计做出贡献
开发者和领域专家共享同一套交流语言,领域专家将知识传递给开发者。开发者总是会离开的,这时培训和工作移交也将变得更加简单,而“只有少数人才了解模型”的情况将大大减少。
4、更好的用户体验
用户体验可以更好地反映出领域模型的好坏。
如果软件留下太多的地方让用户自己去理解,用户往往需要经过培训才能做出操作决定。实际上,用户只是将他们所理解的转移到表单中数据而已。数据将被存储起来,如果用户不知道数据的用途,那么结果也将是错误的。
当用户体验是按照领域专家心中的模型来设计时,就不会出现以上问题了。这时软件本身便能对用户起到培训作用,而不需要业务人员来提供培训。效率提高了,培训减少了——这就是业务价值。
5、清晰的模型边界
我们并不鼓励技术团队将精力单纯地放在编码和算法上,而是期望他们能够面向业务。明确的目标产生高效的解决方案,而要达到这样的目的往往需要更好地理解项目的限界上下文。
6、更好的企业架构
一旦限界上下文得到了较好的理解和仔细的划分,那么团队的所有成员应该知道限界上下文之间的集成是必要的。上下文之间的边界和关系是明晰的。当不同上下文的模型间存在依赖关系时,我们将使用上下文映射图来集成不同的限界上下文,而这又有助于我们全面了解整个企业的架构。
7、敏捷、迭代和持续建模
“设计”这个词可能并不能取悦业务管理层。然而,DDD并不是一个重量级的设计方法和开发过程。DDD并不是画模型图,而是将领域专家的思维模型转化成有用的业务模型。DDD不是创建一个真实世界的模型,而是模仿现实。
团队的工作遵循敏捷方法——迭代式,增量式的。任何一种敏捷方法,只要团队认为合适,都可以用于DDD项目。通过DDD创建出来的模型便是可工作的软件。团队会对模型做持续的改进,直到业务层没有新的需求为止。
8、使用战略和战术新工具
限界上下文为团队创建了一个建模边界,成员在边界内部为特定的业务领域创建解决方案。在单个限界上下文中团队成员共享一套通用语言。不团的团队有时各自负责一个限界上下文,此时可以使用上下文映射图在战略层面上对限界上下文进行界分和集成。
实施DDD所面临的挑战
- 为创建通用语言腾出时间和精力
- 持续地将领域专家引入项目
- 改变开发者对领域的思考方式
为你的业务领域开发一门通用语言便是一个好的出发点。
在DDD中,我们会谈及到对概念的命名。对于概念命名而言,我们有更高层面的要求。当我们对一个领域进行建模时,我们需要仔细地考虑什么样的对象做什么样的事情,这是关于对象行为设计的。我们希望对对象行为的命名能够传达准确的业务含义,也即反应通用语言。要达到这样的目的,肯定不是先在类上定义属性,然后向客户端代码暴露getter和setter那么简单。可以如下方法:
- 对于你目前正在工作的业务领域,思考一下模型中的通用术语和业务操作。
- 将术语写下来。
- 然后,将项目中所用到的短语也写下来。
- 与真正的领域专家交流一下,看看哪些词汇是可以改善的。
开发一个业务子域的指导意见:
- 如果一个限界上下文被当成核心域来开发,那么从战略上来说,这个限界上下文对业务的成功是极其重要的。核心模型是不易理解的,需要不断地尝试和重构。通过持续改进,我们可以延长它的效用生命,这样的做法显然是值得的。当然,这个限界上下文不见得始终是你的核心域。即便如此,如果它是复杂的,创新性的,并且需要在不断的变化中持续存在很长时间,我们还是建议在该限界上下文中使用战术模式。这里,我们假设你的核心域是值得配置最好的开发者的。
- 一个领域,对于消费方来说有可能成为通用子域或者支撑子域,但是却可能成为你自己的核心域。我们并不站在最终消费放的角度来评价一个领域。如果你正在开发的限界上下文是你主要的业务。那么它便是你的核心域,而不管消费方是如何看待的。此时,一定记得使用战术模式。
- 如果你正在开发一个支撑子域,但是由于种种原因,该支撑子域不能从第三方的通用子域直接获得,那么此时战术模式将帮上你大忙。在这种情况下,你需要考虑团队成员的技能水平,还有模型是否具有创新性。如果此时的模型增加了特定的业务价值,而且不只是拥有技术上的绚丽,那么该模型就可以认为是创新性的。如果团队有能力实施战术设计,这个支撑子域又是创新性的,并且将持续存在很长时间,那么此时便是采用战术设计的大好时机。尽管如此,这并不能使该子域成为核心域,因为在业务人士眼中,这样的领域只是支撑性的。
- 团队是否有领域专家,如果有,你如何围绕领域专家组织自己的团队?
- DDD的战术模式是否可以简化与其他限界上下文的集成,不管是第三方还是定制开发的?
- 经验表明,不管是对于哪种开发方式,事务脚本都不能减少代码量。这可能是由于在项目计划阶段,领域复杂性并没有得到正确的认识所致。因此,我们需要在领域复杂性上下足功夫。
- 你的项目进度安排是否允许在战术模式上有所投入?
- 在核心域的战术投入能否消除架构变化所带来的影响?事务脚本是做不到这一点的。和领域模型层相比,其他层更易受到架构变化的影响。
- 客户是否的确能从这种持续设计和开发的方式中获益,或者有现成的产品就能满足他们的需求?换句话说,我们是否应该一开始就考虑定制化开发?
- 使用DDD的战术开发模式会比其他开发方式更加困难吗,比如事务脚本?这个问题很大程度取决于团队成员的技能水平和是否有领域专家。
- 如果团队已经具备了实施DDD的条件,我们还会刻意地选择另一种开发方式吗?有些开发者已经将模型的持久化变得很实用了,比如使用ORM、全聚合序列化和持久化、事件存储(Event Store)、或者战术DDD框架等。但是我们也不能排除还有热衷于其他开发方式的开发者。
有一点需要记住:你最终取悦你的客户,而不是技术开发者,所以你得慎重地做出选择。
DDD并不笨重
DDD绝非是充满繁文缛节的笨重开发过程。事实上,DDD能够很好地与敏捷项目框架结合起来,比如Scrum。DDD也倾向于“测试先行,逐步改进”的设计思路。在你开发一个新的领域对象时,比如实体或值对象,你可以采用以下步骤进行:
- 编写测试代码以模拟客户代码是如何使用该领域对象的。
- 创建该领域对象以使测试代码能够编译通过。
- 同时对测试和领域对象进行重构,知道测试代码能够正确地模拟客户代码,同时领域对象拥有能够表明业务行为的方法签名。
- 实现领域对象的行为,直到测试通过为止,再对实现代码进行重构。
- 向你的团队成员展示代码,包括领域专家,以保证领域对象能够正确地反映通用语言。
你可能会想:“这和我之前采用的测试驱动开发没什么区别呀。”他们可能有细微的区别,但是基本思路是一样的。测试代码并不能保证我们的领域对象就是无懈可击的。之后,我们还会添加另外的测试代码。首先,我们关注的是客户代码如何使用领域对象,此时的测试代码驱动着模型的设计。这种方式和敏捷开发并没有多大区别。因此,即使你并不认为上面的步骤是敏捷,但它们的确表明,DDD采用的是一种“敏捷的”方式进行软件开发的。
在这之后,你会添加更多的测试,从多个角度确保新建领域对象的正确性。此时你关注的是领域对象对于领域概念的表达力,而测试代码本身便是通用语言在程序中的表达。在开发人员的帮助下,领域专家可以通过阅读测试代码来检验领域对象是否满足业务需求。这也意味着测试数据应该是真实的,因为这样可以增加测试代码的业务表达力。否则,领域专家是很难对你的实现做出评判的。