(DDD)实现领域驱动设计
2021-09-13 08:37:35 48 举报
AI智能生成
概括了《实现领域驱动设计》这本书的知识精华
作者其他创作
大纲/内容
初识DDD
领域驱动设计(DDD)作为一种软件开发方法,它可以帮助我们设计高质量的软件模型
DDD首先并不是关于技术的,而是关于讨论、聆听、理解、发现和业务价值的,而这些都是为了将知识集中起来
DDD作为一种软件开发方法,它主要关注以下三个方面
DDD将领域专家和开发人员聚集到一起,这样所开发的软件能够反映出领域专家的思维模型
DDD关注业务战略,战略设计用于清楚地界分不同的系统和业务关注点,这样可以保护每个业务层面的服务。更进一步,这将指引我们如何实现面向服务架构(serviceoriented architecture)或者业务驱动(business-driven architecture)架构
通过使用战术设计建模工具,DDD满足了软件真正的技术需求。这些战术设计工具使开发人员能够按照领域专家的思维模型开发软件。
DDD的作用是简化,而不是复杂化
贫血领域对象是有害的
业务意图不明确
方法的实现本身增加了潜在的复杂性
领域对象根本就不是对象,而只一个数据持有器
在实施DDD时,设计就是代码,代码就是设计
如何DDD
上下文术语:就现在来说,可以将限界上下文看成是整个应用程序之内的一个概念性边界。这个边界之内的每种领域术语、词组或句子——也即通用语言,都有确定的上下文含义。在边界之外,这些术语可能表示不同的意思
通用语言:通用语言是团队共享的语言。领域专家和开发者使用相同的通用语言进行交流,团队中的所有参与者都应该使用通用语言,通用语言是团队自己创建的公用语言,团队中同时包含领域专家和软件开发人员
如何掌握通用语言
通用语言是团队自己创建的公用语言,团队中同时包含领域专家和软件开发人员
创建一个包含简单定义的术语表。将你能想到的术语都罗列出来,包括好的和不好的,并注明好与不好的原因
如果你不喜欢术语表,可以采用其他类型的文档,但记得将那些“不正式”的模型图也包含进去
由于团队中有些人工作在术语表上,还有些人工作在文档上,此时你需要找到团队的其他人员来检查你的成果。分歧肯定是有的,你应该对此有所准备。
每个限界上下文都有自己的通用语言,而有时语言间的术语可能有重叠的地方
使用DDD的业务价值
获得了一个非常有用的领域模型
业务得到了更准确的定义和理解
领域专家可以为软件设计做出贡献
更好的用户体验
清晰的模型边界
更好的企业架构
敏捷、迭代式和持续建模
使用战略和战术新工具
实施DDD所面临的挑战
为创建通用语言腾出时间和精力
持续地将领域专家引入项目
改变开发者对领域的思考方式
领域、子域和限界上下文
问题空间:是领域的一部分,对问题空间的开发将产生一个新的核心域。对问题空间的评估应该同时考虑已有子域和额外所需子域。因此,问题空间是核心域和其他子域的组合。问题空间中的子域通常随着项目的不同而不同,他们各自关注于当前的业务问题,这使得子域对于问题空间的评估非常有用。子域允许我们快速地浏览领域中的各个方面,这些方面对于解决特定的问题是必要的。
解决方案空间:包括一个或多个限界上下文,即一组特定的软件模型。这是因为限界上下文即是一个特定的解决方案,它通过软件的方式来实现解决方案。
限界上下文
限界上下文是显式的,充满语义的
限界上下文主要是一个语义上的边界,我们应该通过这一点来衡量对一个限界上下文的使用正确与否
不需要大而全的模型,模型要限制在限界上下文内
一个限界上下文并不是只包含领域模型
限界上下文主要用来封装通用语言和领域对象,但同时它也包含了那些为领域模型提供交互手段和辅助功能的内容。需要注意的是,对于架构中的每个组件,我们都应该将其放在适当的地方。
核心领域之外的概念不应该包含在限界上下文中。如果一个概念不属于你的通用语言,那么一开始你就不应该将其引入到模型中
上下文映射
上下文映射图的重要性
上下文映射图主要帮助我们从解决方案空间的角度看待问题
识别出项目中的每一个模型并确定它的限界上下文……为每个限界上下文命名,该名字应该是通用语言的一部分。描述模型之间的连接点,将模型间的翻译转换显式地勾勒出来
上下文映射图表现的是项目当前的状态,如果项目会在将来发生变化,你可以到那时才对上下文映射图做相应的更新。关注于当前的项目状态可以帮助你了解你正处的位置,并帮助你决定如何走出下一步
上下文映射图并不是一种企业架构,也不是系统拓扑图。但是,它可以用于高层次的架构分析,指出诸如集成瓶颈之类的架构不足。上下文映射图展现了一种组织动态能力(organizational dynamic),它可以帮助我们识别出有碍项目进展的一些管理问题。
产品和组织关系
合作关系(Partnership):如果两个限界上下文的团队要么一起成功,要么一起失败,此时他们需要建立起一种合作关系。他们需要一起协调开发计划和集成管理。两个团队应该在接口的演化上进行合作以同时满足两个系统的需求。应该为相互关联的软件功能制定好计划表,这样可以确保这些功能在同一个发布中完成。
共享内核(Shared Kernel):对模型和代码的共享将产生一种紧密的依赖性,对于设计来说,这种依赖性可好可坏。我们需要为共享的部分模型指定一个显式的边界,并保持共享内核的小型化。共享内核具有特殊的状态,在没有与另一个团队协商的情况下,这种状态是不能改变的。我们应该引入一种持续集成过程来保证共享内核与通用语言(1)的一致性。
客户方-供应方开发(Customer-Supplier Development):当两个团队处于一种上游-下游关系时,上游团队可能独立于下游团队完成开发,此时下游团队的开发可能会受到很大的影响。因此,在上游团队的计划中,我们应该顾及到下游团队的需求。
遵奉者(Conformist):在存在上游-下游关系的两个团队中,如果上游团队已经没有动力提供下游团队之所需,下游团队便孤军无助了。出于利他主义,上游团队可能向下游团队做出种种承诺,但是有很大的可能是:这些承诺是无法实现的。下游团队只能盲目地使用上游团队的模型。
防腐层(Anticorruption Layer):在集成两个设计良好的限界上下文时,翻译层可能很简单,甚至可以很优雅地实现。但是,当共享内核、合作关系或客户方-供应方关系无法顺利实现时,此时的翻译将变得复杂。对于下游客户来说,你需要根据自己的领域模型创建一个单独的层,该层作为上游系统的委派向你的系统提供功能。防腐层通过已有的接口与其他系统交互,而其他系统只需要做很小的修改,甚至无须修改。在防腐层内部,它在你自己的模型和他方模型之间进行翻译转换。
开放主机服务(Open Host Service):定义一种协议,让你的子系统通过该协议来访问你的服务。你需要将该协议公开,这样任何想与你集成的人都可以使用该协议。在有新的集成需求时,你应该对协议进行改进或者扩展。对于一些特殊的需求,你可以采用一次性的翻译予以处理,这样可以保持协议的简单性和连贯性。
发布语言(Published Language):在两个限界上下文之间翻译模型需要一种公用的语言。此时你应该使用一种发布出来的共享语言来完成集成交流。发布语言通常与开放主机服务一起使用。
另谋他路(SeparateWay):在确定需求时,我们应该做到坚决彻底。如果两套功能没有显著的关系,那么它们是可以被完全解耦的。集成总是昂贵的,有时带给你的好处也不大。声明两个限界上下文之间不存在任何关系,这样使得开发者去另外寻找简单的、专门的方法来解决问题。
大泥球(Big Ball of Mud):当我们检查已有系统时,经常会发现系统中存在混杂在一起的模型,它们之间的边界是非常模糊的。此时你应该为整个系统绘制一个边界,然后将其归纳在大泥球范围之列。在这个边界之内,不要试图使用复杂的建模手段来化解问题。同时,这样的系统有可能会向其他系统蔓延,你应该对此保持警觉。
在上下文映射图中,我们使用以下缩写来表示各种关系
ACL表示防腐层
OHS表示开放主机服务
PL表示发布语言
我们只需要有限的RPC调用或REST请求。然而,要与远程模型保持同步,最好的方式是在远程系统中采用面向消息的通知(notification)机制。消息通知可以通过服务总线进行发布,也可以采用消息队列或者REST。
架构
分层架构
在分层架构中,我们将领域模型和业务逻辑分离出来,并减少对基础设施、用户界面甚至应用层逻辑的依赖,因为它们不属于业务逻辑。将一个复杂的系统分为不同的层,每层都应该具有良好的内聚性,并且只依赖于比其自身更低的层。
原则
严格分层架构(Strict Layers Architecture)中,某层只能与直接位于其下方的层发生耦合;
松散分层架构(Relaxed Layers Architecture)则允许任意上方层与任意下方层发生耦合
DDD经典分层
用户接口层
应用层
领域层
基础设施层
通过依赖导致实现底层调用上层
有趣的是,当我们在分层架构中采用依赖倒置原则时,我们可能会发现,事实上已经不存在分层的概念了。无论是高层还是低层,它们都只依赖于抽象,好像把整个分层架构给推平了一样。
六边形架构(端口与适配器)
六边形架构也称为端口与适配器。对于每种外界类型,都有一个适配器与之相对应。外界通过应用层API与内部进行交互。
六边形架构的一大好处在于,我们可以轻易地开发用于测试的适配器。整个应用程序和领域模型可以在没有客户和存储机制的条件下进行设计开发。
SOA
面向服务架构(Service-Oriented Architecture,SOA)对于不同的人来说具有不同的意思。
SOA宣言给我的感觉是,我既可以将服务看作一系列的SOAP/WSDL接口,也可以将其看成为一组REST资源
REST
REST作为一种架构风格
架构风格之于架构就像设计模式之于设计一样。它将不同架构实现所共有的东西抽象出来,使得我们在谈及到架构时不至于陷入技术细节中
RESTful HTTP服务器的关键方面
首先,就像其名字所指出的,资源是关键的概念。
另一个关键方面是无状态通信,此时我们将采用具有自描述功能的消息。无状态通信保证了不同请求之间的相互独立性,这在很大程度上提高了系统的可伸缩性。
最后,通过使用超媒体(Hypermedia),REST服务器的客户端可以沿着某种路径发现应用程序可能的状态变化。
RESTful HTTP客户端的关键方面
客户端可以通过两种方式在不同资源之间进行转移,一种是上面所提到的超媒体,一种是服务器端的重定向
REST和DDD
不建议将领域模型直接暴露给外界,因为这样会使系统接口变得非常脆弱,原因在于对领域模型的每次改变都会导致对系统接口的改变
DDD与RESTful HTTP合并起来使用,我们有两种方式
第一种方法是为系统接口层单独创建一个限界上下文,再在此上下文中通过适当的策略来访问实际的核心模型
另一种方法用于需要使用标准媒体类型的时候。如果某种媒体类型并不用于支持单个系统接口,而是用于一组相似的客户端-服务器交互场景,此时我们可以创建一个领域模型来处理每一种媒体类型。
为什么是REST
符合REST原则的系统将具有更好的松耦合性
基于REST的系统也是非常容易理解的,因为此时系统被分为很多较小的资源块
具有很好的松耦合性和可伸缩性。
CQRS
一个方法要么是执行某种动作的命令,要么是返回数据的查询,而不能两者皆是。
CQRS旨在解决数据显示复杂性问题,而不是什么绚丽的新风格以使你的简历增光添彩。
查询模型也被称为读模型,同样,命令模型也被称为写模型。
事件驱动架构
事件驱动架构(Event-Driven Architecture,EDA)是一种用于处理事件的生成、发现和处理等任务的软件架构。
基于消息的系统通常呈现出一种管道和过滤器风格。
管道和过滤器
长时处理过程(也叫Saga)
将处理过程设计成一个组合任务,使用一个执行组件对任务进行跟踪,并对各个步骤和任务完成情况进行持久化
将处理过程设计成一组聚合,这些聚合在一系列的活动中相互协作。一个或多个聚合实例充当执行组件并维护整个处理过程的状态。
设计一个无状态的处理过程,其中每一个消息处理组件都将对所接收到的消息进行扩充——即向其中加入额外的数据信息——然后再将消息发送到下一个处理组件。在这种方法种,整个处理过程的状态包含在每条消息中。
执行器和跟踪器
事件源
对于某个聚合上的每次命令操作,都有至少一个领域事件发布出去,该领域事件描述了操作的执行结果。每一个领域事件都将被保存到事件存储(Event Store,8)中
分布式计算
数据缓存
数据复制
持续查询
分布式处理
实体
为什么使用实体
当我们需要考虑一个对象的个性特征,或者需要区分不同的对象时,我们引入实体这个领域概念。一个实体是一个唯一的东西,并且可以在相当长的一段时间内持续地变化。
工作重点不应该是数据库
唯一标识
在设计实体时,我们首先需要考虑实体的本质特征,特别是实体的唯一标识和对实体的查找,而不是一开始便关注实体的属性和行为。只有在对实体的本质特征有用的情况下,才加入相应的属性和行为
值对象可以用于存放实体的唯一标识。值对象是不变(immutable)的,这可以保证实体身份的稳定性,并且与身份标识相关的行为也可以得到集中处理。
常用的创建实体身份标识的策略
用户提供一个或多个初始唯一值作为程序输入,程序应该保证这些初始值是唯一的。
程序内部通过某种算法自动生成身份标识,此时可以使用一些类库或框架,当然程序自身也可以完成这样的功能。
程序依赖于持久化存储,比如数据库,来生成唯一标识。
另一个限界上下文(2)(系统或程序)已经决定出了唯一标识,这作为程序的输入,用户可以在一组标识中进行选择。
将实体标识和委派标识区分
实体及其本质特征
挖掘实体的重要属性
挖掘实体的关键行为
角色和职责
建模的一个方面便是发现对象的角色和职责。通常来说,对角色和职责分析是可以应用在领域对象上的。这里我们特别关注的是实体的角色和职责。
创建
验证
验证属性
验证整个对象
验证对象组合
跟踪变化
领域事件
事件存储
创建一个单独的组件来完成模型验证,不能将验证逻辑嵌入实体
个人总结:实体存在的终极意义,应该是为了能够通过打磨模型来表达业务逻辑,有了实体才能完成这个过程
值对象
我们应该尽量使用值对象来建模而不是实体对象
值对象的特征
在将领域概念建模成值对象时,我们应该将通用语言考虑在内,这是建模值对象的首要原则
当你决定一个领域概念是否是一个值对象时,你需要考虑它是否拥有以下特征
它度量或者描述了领域中的一件东西
它可以作为不变量
它将不同的相关的属性组合成一个概念整体
当度量和描述改变时,可以用另一个值对象予以替换
它可以和其他值对象进行相等性比较
它不会对协作对象造成副作用
我们应该尽量地使值对象只依赖于它自己的属性,并且只理解它自身的状态。虽然在有些情况下这并不可行,但这是我们的目标。
我们传给值对象方法的参数依然应该是值对象
最小化集成
在有可能的情况下使用值对象来完成限界上下文之间的集成,这对于许多需要消费标准类型的上下文来说都是适用的。
用值对象表示标准类型
值对象便是建模度量和描述概念的最佳方式
在限界上下文中,如果没有必要维护一个描述类型对象的生命周期,那么请将其建模成值对象
使用Java的枚举则是非常简单的,与通过状态模式来创建标准类型相比,枚举可能是更好的方法。我认为这里我们同时得到了两种方法的好处。一方面我们获得了一个非常简单的标准类型,另一方面又能有效地表示当前的状态
建议尽量使用枚举来表示标准类型,即便你认为某个标准类型更像一种状态模式。如果存在大量的标准类型实例,我们可以考虑通过代码生成来创建枚举
测试值对象
在创建测试时,我们应该保证领域专家能够读懂这些测试,即测试应该具有领域含义
实现
只有主构造函数才能使用自委派性来设置属性值,除此之外,其他方法都不能使用setter方法
在设计值对象时,我们也不应该完全地遵循JavaBean规范。比如,JavaBean规范要求我们提供公有的setter方法,而这将违背值对象的不变性特征
一个对象的setter和getter方法并不见得只局限于设置对象的属性值,还可以进行断言[Evans]操作,这对于通常的软件开发和DDD模型来说都是很重要的
守卫断言能够,并且应该用于任何有可能接受错误参数的地方。
持久化值对象
拒绝由数据建模泄漏带来的不利影响
面临这种阻抗失配时,你应该从领域模型的角度,而不是持久化的角度去思考问题
问问自己以下几个问题
我当前所建模的概念表示领域中的一个东西呢,还是只是用于描述和度量其他东西?
如果该概念起描述作用,那么它是否满足先前所提到的值对象的几大特征?
将该概念建模成实体是不是只是持久化机制上的考虑?
将该概念建模成实体是不是因为它拥有唯一标识,我们关注的是对象实例的个体性,并且需要在其整个生命周期中跟踪其变化?
在可能的情况下,尽量根据领域模型来设计数据模型,而不是根据数据模型来设计领域模型
DDD不是关于如何根据范式来组织数据的,而是在一个一致的限界上下文中建模一套通用语言。在这个过程中,你应该尽量地避免数据模型从你的领域模型中泄漏到客户端中
采用隐藏委派主键的方式将值对象集合映射成一对多的关系要简单得多
尽量使用值对象的重要性
易于开发、测试和维护
领域服务
我们应该尽量避免在聚合中使用资源库
用一个单一的建模工具就可以解决,即领域服务(Domain Service)
什么是领域服务
不要将领域服务与应用服务混杂在一起了。在应用服务中,我们并不会处理业务逻辑,但是领域服务却恰恰是处理业务逻辑的
当领域中的某个操作过程或转换过程不是实体或值对象的职责时,此时我们便应该将该操作放在一个单独的接口中,即领域服务。请确保该领域服务和通用语言是一致的;并且保证它是无状态的。
领域模型主要关注于特定于某个领域的业务,同样,领域服务也具有相似的特点。由于领域服务有可能在单个原子操作中处理多个领域对象,这将增加领域服务的复杂性。
你可以使用领域服务来
执行一个显著的业务操作过程
对领域对象进行转换
以多个领域对象作为输入进行计算,结果产生一个值对象
确保领域服务是无状态的,并且能够明确地表达限界上下文中的通用语言(1)
不要过于倾向于将一个领域概念建模成领域服务,而是只有在有必要的时候才这么做。一不小心,我们就有可能陷入将领域服务作为“银弹”的陷进。过度地使用领域服务将导致贫血领域模型[Fowler,Anemic],即所有的业务逻辑都位于领域服务中,而不是实体和值对象中
在有可能的情况下,我们应该尽量使建模术语直接地表达出团队成员的交流用语。
独立接口对于解偶来说是有用处的,此时客户端只需要依赖于接口,而不需要知道具体的实现
与服务工厂和依赖注入相比,有时他们更倾向于将领域服务作为构造函数参数或者方法参数传入[2]
绝对不能将业务逻辑放到应用层
应用服务中,我们关心的是事务和安全,但是这些不应该出现在领域服务中
不要滥用领域服务。 滥用领域服务将导致贫血领域模型这种反模式。
领域事件
领域事件(Domain Event)来捕获发生在领域中的一些事情。领域事件是一个功能强大的建模工具,一旦你使用了它,你便无法释手了。在一开始使用领域事件时,你要做的是对不同的事件进行定义
领域专家所关心的发生在领域中的一些事件
当团队成员对领域事件达成一致之后,领域事件便是通用语言的正式组成部分了。
事件所携带的属性能够反映出该事件的来源。多数事件的构造函数都只允许全状态初始化,同时,事件对象还提供了访问不同属性的getter方法。
当我们需要将领域事件发布到外部限界上下文中时,为事件创建唯一标识也是有必要的。
一种简单高效的发布领域事件的方法便是使用观察者(Observer)模式[Gammaet al.],这种方法可以在领域模型和外部组件之间进行解耦。出于命名的原因,我将使用“发布-订阅”来表示该模式
由什么组件向领域事件注册订阅方呢?通常来说,这种功能由应用服务(14)完成,有时也由领域服务完成。
所有聚合实例之间的最终一致性必须通过异步的方式予以处理。
我们如何保证领域模型存储和事件存储之间一致性呢?有三种基本的方式
领域模型和消息设施共享持久化存储(比如,数据源)
领域模型的持久化存储和消息持久化存储由全局的XA事务(两阶段提交)所控制
在领域模型的持久化存储中,创建一个特殊的存储区域(比如一张数据库表),该区域用于存储领域事件
通过使用领域事件,我们可以将任何企业系统设计成自治服务和系统。这里的自治服务表示一个设计良好的业务服务,我们可以将其看成一个系统或者应用程序。在
领域事件将携带有限的命令参数和聚合状态,这些信息足以使作为订阅方的限界上下文做出相应的操作
对于自治服务和支持它们的消息设施来说,我们应该在可用性和可伸缩性上下足功夫,以便更好地完成那些非功能性的需求。
REST风格事件通知的优缺点:
如果多个客户方都可以通过单个URI来请求相同的事件通知,那么此时REST便是合适的
如果一个或多个消费方需要从多个发布方中获取资源以顺序地完成一系列任务,那么此时你便会感到REST所带来的痛苦了
事件通知是一个应用程序级别上的关注点,而不是领域的关注点,即便这些事件通知是源自于领域模型的也是如此。
处理重复消息的一种方式便是将订阅方的处理过程变成幂等操作过程。订阅方对消息的处理对于其自己的领域模型来说应该是幂等的。
模块
模块中的目的在于达到松耦合性
我们应该将模块看作模型中的一等公民,在设计和命名上应该给予和实体(5)、值对象(6)、领域服务和领域事件(8)同等的重视程度。这意味着在有必要为模块重命名时,我们就应该为其重命名,并且按需地、及时地将领域概念添加到模块中。
模块都具有一种层级形式
先考虑模块,再是限界上下文
通过通用语言的需求来划分模型边界
使用模块的目的在于组织那些内聚在一起的领域对象,对于那些内聚性不强或者没有内聚性的领域对象来说,我们应该将它们划分在不同的模块中。
聚合
有很多途径都将导致我们建立不正确的聚合模型。一方面,我们可能为了对象组合上的方便而将聚合设计得很大。另一方面,我们设计的聚合又可能因为过于贫瘠而丧失了保护真正不变条件的目的。我们应该同时避免这两个极端,转而将注意力集中在业务规则上。
聚合模式讨论的是对象组合和信息隐藏,聚合模式还包含了一致性边界和事务
聚合设计的原则
在一致性边界之内建模真正的不变条件
这里的不变条件表示一个业务规则,该规则应该总是保持一致的。存在多种类型的一致性,其中之一便是事务一致性,事务一致性要求立即性和原子性。同时,还存在最终一致性。
聚合表达了与事务一致性边界相同的意思
设计聚合时,我们主要关注的是聚合的一致性边界,而不是创建一个对象树。
设计小聚合
好的做法是,使用根实体(Root Entity)来表示聚合,其中只包含最小数量的属性或值类型属性[3]。这里的“最小数量”表示所需的最小属性集合,不多也不少。
很多情况下,许多建模成实体的概念都可以重构成值对象。优先选用值对象并不意味着聚合就是不变的,因为当值对象属性被替换成其他值时,根实体也就随之改变了。
小聚合不仅有性能和可伸缩性上的好处,它还有助于事务的成功执行,即它可以减少事务提交冲突。这样一来,系统的可用性也得到了增强。在你的领域中,迫使你设计大聚合的不变条件约束并不多。当你遇到这样的情况时,可以考虑添加实体或者是集合,但无论如何,我们都应该将聚合设计得尽量小。
小聚合可以增强模型的性能和可伸缩性,另外它还有助于创建分布式系统。
通过唯一标识引用其他聚合
通过标识引用使多个聚合协同工作
在调用聚合行为方法之前,使用资源库或领域服务(7)来获取所需要的对象
通过应用服务来处理依赖关系可以避免在聚合中使用资源库或领域服务。然而,如果要处理特定于领域的复杂依赖关系,在聚合的命令方法中使用领域服务却是最好的方法。
如果对聚合的查询导致了性能问题,那么我们可以考虑theta联合查询或者CQRS。而如果CQRS和theta联合查询都不能满足我们的需求,那么就需要在标识引用和直接引用之间折中考虑了。
在边界之外使用最终一致性
当在一个聚合上执行命令方法时,如果还需要在其他的聚合上执行额外的业务规则,那么请使用最终一致性。
从长远看来,遵循聚合原则对整个项目是有益的。我们将尽可能地保证一致性,并且致力于创建高性能的、高可伸缩性的系统。
实现
创建具有唯一标识的根实体
优先使用值对象
使用迪米特法则
强调了“最小知识”原则
客户端对象使用服务对象时,它应该尽量少地知道服务对象的内部结构
对迪米特法则做一个简单的总结:任何对象的任何方法只能调用以下对象中的方法:(1)该对象自身,(2)所传入的参数对象,(3)它所创建的对象,(4)自身所包含的其他对象,并且对那些对象有直接访问权。
告诉而非询问原则
客户端对象不应该首先询问服务对象,然后根据询问结果调用服务对象中的方法,而是应该通过调用服务对象的公共接口的方式来“告诉”服务对象所要执行的操作
乐观并发
避免依赖注入
工厂
工厂应该提供一个创建对象的接口,该接口封装了所有创建对象的复杂操作过程,同时,它并不需要客户去引用那个实际被创建的对象。对于聚合来说,我们应该一次性地创建整个聚合,并且确保它的不变条件得到满足
除了创建对象之外,工厂并不需要承担领域模型中的其他职责
团队成员采用了能够表达通用语言的工厂方法名。这样,领域专家和团队成员都可以使用相同的语言进行交流
领域服务中的工厂
聚合根中的工厂方法
领域模型中的工厂
资源库
当你从资源库中取出一个物品时,你希望该物品和其先前存放时的状态是一样的。有时,你有可能会从资源库中移除某些物品。
通常我们将聚合(10)实例存放在资源库中,之后再通过该资源库来获取相同的实例
每一种聚合类型都将拥有一个资源库。通常来说,聚合类型和资源库之间存在着一对一的关系。然而有时,当两个或多个聚合位于同一个对象层级中时,它们可以共享同一个资源库
严格来讲,只有聚合才拥有资源库
如果你只是随机地、直接地获取和使用实体(5),而不用考虑聚合的事务边界,那么你可以不考虑使用资源库
面向集合资源库
一个资源库应该模拟一个Set集合。无论采用什么类型的持久化机制,我们都不应该允许多次添加同一个聚合实例。另外,当从资源库中获取到一个对象并对其进行修改时,我们并不需要“重新保存”该对象到资源库中
将资源库接口定义放在了与聚合相同的包中,而将资源库的实现类放在了impl子包中,这种方式被大量的Java项目所采用。然而,在协作上下文中,团队成员们将实现类放在了基础设施层中:
我们应该避免使用一对一关联,而应该使用多对一的单向关联
强烈反对由聚合来管理持久化,同时我强烈地提倡只使用资源库来处理持久化
这正是使用资源库的正确方法——要么只读,要么读取就是为了修改
面向持久化资源库
慎重使用这种方式:如果我们要获取聚合根下的某些子聚合,我们不用先从资源库中获取到聚合根,然后再从聚合根中获取这些子聚合,而是可以直接从资源库中返回
用例优化查询(Use CaseOptimal Query)的方法直接查询所需要的数据。此时,我们可以直接在持久化机制上执行查询,然后将查询结果放在一个值对象(6)中予以返回。从资源库中返回值对象而非聚合实例并不奇怪。
不要过度地在领域模型上使用事务。我们必须慎重地设计聚合以保证正确的一致性边界。
在设计资源库时,我们应该采用面向集合的方式,而不是面向数据访问的方式。
集成限界上下文
集成基础知识
其中一种便是在一个限界上下文中暴露应用程序编程接口(API),然后在另一个限界上下文中通过远程过程调用(RPC)的方式访问该API。
另一种直接的集成方式便是使用消息机制。
第三种集成限界上下文的方式是使用RESTful HTTP。有人认为REST也是某种形式的RPC,其实并非如此。
还可以通过共享文件和数据库的方式进行集成,但是此时我只能说,你也太不与时俱进了。
分布式计算原则
• 网络是不可靠的。
• 总会存在时间延迟,有时甚至非常严重。
• 带宽是有限的。
• 不要假设网络是安全的。
• 网络拓扑结构将发生变化。
• 知识和政策在多个管理员之间传播。
• 网络传输是有成本的。
• 网络是异构的。
作为消费方的限界上下文需要关心的只应该是数据属性,而不是外部模型所提供的功能。
任何计算和处理过程都应该在生产方限界上下文中完成,然后向消费方提供足够的事件数据。
在项目开始时,我们可以使用部署接口和类的方式,但是在产品环境下,使用低耦合的自定义媒体类型(JSON)契约则更好。
通过REST资源集成限界上下文
当一个限界上下文以URI的方式提供了大量的REST资源时,我们便可称其为开放主机服务
RESTful HTTP,他们认为这种方式的好处在于不用向客服方暴露自身领域模型的结构和行为细节
开放主机服务的部分定义的:“在有新的集成需求时,对协议进行改进和扩展。”
使用防腐层实现REST客户端
资源库通常是用来持久化和重建聚合的,将其用于创建值对象似乎就不合适了。当然,如果我们的目的就是要通过防腐层创建聚合,那么资源库便是一种自然的选择。
通过消息集成限界上下文
在使用消息进行集成时,任何一个系统都可以获得更高层次的自治性。只要消息基础设施工作正常,即使其中一个交互系统不可用,消息依然可以得到发送和投递。
在DDD中,增强系统自治性的一种方式便是使用领域事件。
我们需要考虑到事件的唯一性以便对每个事件进行记录。当事件发生时,系统将通过消息机制将这些事件发送到对事件感兴趣的相关方。
职责分离却并非琐碎之事,至少在使用消息机制时是这样的
需要考虑到消息无序抵达的情况和多次投递的情况[1]
在有可能的情况下,我们应该最小化不同限界上下文之间的信息复制,甚至彻底消除。当然,要完全避免信息复制是不可能的
长时处理过程
对于在不同限界上下文之间复制信息时所要求的职责来说,我们将予以拒绝。我们将使那些记录数据的系统自行处理自己的信息。
事件存储是专门为产生该事件的限界上下文创建的。所有新加
于任何基础设施和架构所带来的影响,幂等操作都将予以忽略,并且是无害的
长时处理过程的状态机和超时跟踪器
重复发送过程直到成功为止,比如可以使用盖帽指数后退算法
在消息机制不可用时,通知的发布方将不能通过该消息机制发布事件。这种情况将被发布客户端所检测到,此时的客户端可以退一步,减少消息的发送量,等到消息系统可用时再进行正常发送。
应用程序
领域模型通常位于应用程序的中心位置
当一个应用程序通过集成的方式依赖于其他应用程序或者服务时,整个解决方案便可以称为一个系统。有时,应用程序和系统表示的是相同的概念,即当我们说到“应用程序”时,我们也完全可以称为“系统”
用户界面
渲染领域对象
渲染数据传输对象
DTO:它的缺点在于,我们需要创建一些与领域对象非常相似的类。另外,我们需要创建一些必须由虚拟机(比如JVM)所管理的大对象,而事实上这些对象却与单虚拟机应用架构不相匹配。
在使用DTO时,我们的聚合设计需要考虑到DTO组装器对聚合数据的查询。此时,我们需要慎重考虑,因为我们不应该暴露出太多的聚合内部结构。
使用调停者发布聚合的内部状态
要解决客户端和领域模型之间的耦合问题,我们可以使用调停者模式
聚合将通过调停者接口来发布内部状态。客户端将实现调停者接口,然后把实现对象的引用作为参数传给聚合。之后,聚合双分派给调停者以发布自身状态,在这个过程中,聚合并没有向外暴露自身的内部结构。这里的诀窍在于,不要将调停者接口与任何显示规范绑定在一起,而是关注于对所感兴趣的聚合状态的渲染
DPO
在没有必要使用DTO时,我们可以使用另一种改进方法。该方法将多个聚合实例中需要显示的数据汇集到一个领域负载对象(Domain Payload Object,DPO)
DPO中包含了对整个聚合实例的引用,而不是单独的属性。
通过资源库获取到所需聚合实例,然后创建DPO实例,该DPO持有对所有聚合实例的引用
由于DPO持有的是对整个聚合实例的引用,延迟加载的对象/集合并未被加载到内存中
要解决延迟加载的问题,我们可以选择即时加载,或者使用领域依赖求解器(Domain Dependency Resolver,DDR)
我们应该基于用例来创建状态展现,而不是基于聚合实例。从这一点来看,创建状态展现和DTO是相似的,因为DTO也是基于用例的。然而,更准确的是将一组REST资源看作一个单独的模型——视图模型(ViewModel)或展现模型(Presentation Model)
我们所创建的展现模型不应该与领域模型中的聚合状态存在一一对应的关系。否则,你的客户端便需要像聚合本身一样了解你的领域模型。此时,客户端需要紧跟领域模型中行为和状态的变化,你也随之失去了抽象所带来的好处。
我们可以转而使用用例优化查询。此时,我们可以在资源库中创建一些查询方法,这些方法返回的是所有聚合实例属性的超集。查询方法动态地将查询结果放在一个值对象(6)中,该值对象是特别为当前用例设计的。请注意,你设计的是值对象,而不是DTO,因为此时的查询是特定于领域的,而不是特定于应用程序的。这个用例优化的值对象将被直接用于渲染用户界面。
用例优化查询的动机与CQRS(4)相似
我们可以将展现模型看成是一种适配器[Gamma et al.]。它根据视图之所需向外提供属性和行为,由此隐藏了领域模型的细节。
我们不会要求领域模型对视图显示属性提供特别的支持,而是将职责分给展现模型。此时,展现模型通过领域模型的状态推导出一些特定于视图的指示器和属性值。
我们不应该在展现模型中出现使用应用服务的细节,或者甚至直接将展现模型本身作为领域模型的应用服务。我们希望看到的是,在展现模型中简单地将处理逻辑委派给更复杂、更重量级的门面
应用服务
应用服务负责用例流的任务协调,每个用例流对应了一个服务方法。在使用ACID数据库时,应用服务还负责控制事务以确保对模型修改的原子提交。另外,应用服务还会处理和安全相关的操作。
将应用服务与领域服务(7)等同起来是错误的。
我们应该将所有的业务领域逻辑放在领域模型中,不管是聚合、值对象或者领域服务;而将应用服务做成很薄的一层,并且只使用它们来协调对模型的任务操作。
可以使用单个标准输出端口,然后为不同种类的客户端创建不同的适配器
基础设施
资源库的实现被放在了基础设施层中,因为它们负责处理数据存储,而这些不属于模型的职责。你可以使基础设施层实现那些与消息相关的接口,比如消息队列和E-mail等。如果还有一些特殊的用户界面组件来处理诸如图表之类的展现,那么它们也应该放在基础设施层中。
0 条评论
下一页