第二章:模型驱动设计MDA的实现

  这些模式根据领域驱动的设计投射了广泛的面向对象设计的最佳实践。他们指导决策以澄清模型,并使模型和实施保持一致,每一个都强化了对方的有效性。精心设计各个模型元素的细节为开发人员提供了一个稳定的平台,可以从中探索模型并使其与实现密切相关。

 

分层架构

  在面向对象的程序中,UI,数据库和其他支持代码通常直接写入业务对象(DTO数据传输对象)。其他业务逻辑嵌入在UI小部件和数据库脚本的行为中。发生这种情况是因为从短期来看,这是使事情顺利进行的最简单方法。(数据表生成大量临时数据对象DTO的CRUD增删改查是最简单开发方法)

  当与领域相关的代码通过如此大量的其他代码(DTO或VO)进行传播时,很难看到和逻辑推理。UI的表面更改实际上导致改变业务逻辑;要更改业务规则,可能需要对UI代码、数据库代码或其他程序元素进行细致的跟踪。实现连贯的,模型驱动的对象变得不切实际;自动化测试很尴尬,由于每项活动都涉及所有技术和逻辑,因此必须保持程序非常简单或无法理解。

因此:

  隔离领域模型和业务逻辑的表达式,消除对基础架构、用户界面甚至非业务逻辑的应用程序逻辑的依赖性。将复杂程序划分为多个层,在每个层内开发一个具有凝聚力的设计,并且仅影响下面的层。遵循标准的架构模式,为上面的层提供松散的耦合。在一个层中集中与领域模型相关的所有代码,并将其与用户界面、应用程序和基础结构代码隔离开来。领域对象不受显示自身,存储自身,管理应用程序任务等的责任,可以专注于表达领域模型。这使得模型能够发展到足够丰富和清晰,以捕获必要的业务知识并使其发挥作用。

  这里的关键目标是隔离,相关模式,例如“六边形体系结构”(banq注:参考鲍勃大叔的清晰架构),可以在允许我们的领域模型表达中避免依赖和引用其他系统问题的程度上提供或改善。

 

实体

  许多对象代表一个系列事物:有连续性、有标识的、经历一个生命周期,尽管它的属性可能会改变。(banq注:可以用数据表中的实体概念类比理解,两者相差不多)

  某些对象主要不是由其属性定义的,它们代表了一个贯穿时间并经常跨越不同表示的有标识的系列事务。有时,即使属性不同,这样的对象也必须与另一个对象匹配,必须将对象与其他对象区分开来,即使它们可能具有相同的属性,错误的标识可能导致数据损坏。

因此:

  当一个对象通过其标识而不是其属性进行区分时,将这种初心一直保持在模型的定义中。保持类的定义简单,重点关注生命周期的连续性和标识。

  定义区分每个对象的方法,无论其形式或历史如何,警惕需要按属性匹配对象的需求。定义一个能保证为每个对象生成唯一结果的操作方法,也可以通过附加保证唯一的符号如(主健ID)。这种识别方法可能来自外部,也可能是由系统创建的任意标识符,但它必须与模型中的标识区别相对应。

  模型必须定义那些意味着相同的东西。(banq注:参考类Class的定义,参考用汽车比喻理解OOP:)

#DDD实体


值对象

  一些对象用于描述或计算事物的某些特征。

  许多物体无需进行概念上的认同和区分,无此必要,实体的缺点是带来设计的复杂性。

  跟踪实体的标识至关重要,但将标识ID附加到其他对象可能会损害系统性能,增加分析工作量。因此使所有对象看起来相同来混同模型的区别,有时这样反而简单,软件设计与复杂性是一场持续的战斗。我们必须做出实体和值对象区分,不能将所有对象都看成实体,默认情况为值对象,仅在必要时才使用特殊处理。

  但是,如果我们认为这类对象只是缺乏标识,那么我们的工具箱或词汇量就没有增加太多,实际上,这些对象具有自己的特征,以及它们对模型的重要意义,这些是描述事物特征的对象。

因此:

  如果只关心模型元素的属性和逻辑(而不是区分它们),则将其归类为值对象。使它表达它传达的属性的含义并赋予它相关的功能。将值对象视为不可变;使所有操作无副作用 - 不依赖于任何可变状态的函数。不要为值对象赋予任何标识,并避免维护实体带来的设计复杂性。

#DDD值对象

 

领域事件

  领域专家关心发生了的事情。

  实体负责跟踪其状态和规范其生命周期的业务规则。但是如果你需要知道状态变化的实际原因,只记录状态是无法明确原因的,并且可能很难解释系统如何得到这个状态结果逻辑推理过程,审计跟踪可以实现跟踪,但通常不适合用于程序本身的逻辑,实体的更改历史记录可以允许访问先前的状态,但如果忽略保留这些更改的含义,任何对信息的任何操作都是程序性的、连续的,这些含义会被推出领域层。

  分布式系统中也出现了一系列独特但相关的问题,分布式系统的状态不能始终保持完全一致(只能最终一致),但是我们必须始终保持聚合内部状态一致,同时异步通知其他聚合进行更改,当这种更改消息在网络的节点之间传播时,可能难以解决无序到达或来自不同源的多个更新(事件消息的顺序问题)。

因此:

  将有关领域中发生的事情信息建模为一系列离散事件,将每个事件表示为领域对象,这些与反映软件本身内活动的系统事件不同,尽管系统事件通常与领域事件相关联,或者作为对领域事件的响应的一部分,或者作为将领域事件的信息传递到系统中 。

  领域事件是领域模型的完整部分,表示领域中发生的事情。忽略不相关的领域活动,同时明确表示领域专家想要跟踪或被通知的事件,或者与其他模型对象中的状态更改相关联的事件。

  在分布式系统中,可以从特定节点当前已知的领域事件推断出实体的状态,从而在缺乏关于整个系统的完整信息的情况下实现相干模型。

  领域事件通常是不可变的,因为它们是过去某些事物的记录,除了对事件的描述之外,领域事件通常还包含事件发生时间的时间戳以及事件中涉及的实体的标识。此外,领域事件通常具有单独的时间戳,指示事件何时进入系统以及输入事件的人员的身份。领域事件的标识可以基于这些属性的某些集合。因此,例如,如果同一事件的两个实例到达节点,则可以将它们识别为相同。
(banq注:参考领域事件,下图是Jdon框架通过领域事件实现干净的架构)

 

服务

  有时,它不是一件事。

  来自领域的一些概念不能自然地建模为对象,如果强制所需的领域功能成为实体或值的责任,要么扭曲基于模型的对象的定义,要么添加无意义的人造对象。

因此:

  当领域中的重要流程或转换不是实体或值对象的自然责任时,将操作作为声明为服务的独立接口添加到模型,定义服务契约,一组关于与服务交互的断言。在特定有界上下文的普遍存在的语言中陈述这些断言。为服务命名,这也成为普遍存在的语言的一部分。

# DDD服务


模块

  每个人都使用模块,但很少有人将它们视为模型的完整部分,代码被分解为各种类别,从技术架构的各个方面到开发人员的工作分配,即使是很多重构的开发人员也倾向于满足于项目早期构想的模块。

  耦合和内聚的解释倾向于使它们听起来像技术指标,根据关联和相互作用的分布进行机械判断,然而,不仅仅是代码被分为模块,还有概念。一个人一次可以考虑多少事情是有限的(因此需要低耦合),不连贯的思想碎片就像一种无差别的心灵鸡汤一样难以理解。

因此:

  选择讲述系统故事的模块,并包含一组紧密结合的概念,为模块命名,使其成为普遍存在的语言的一部分,模块是模型的一部分,它们的名称应该反映对领域的洞察力。

  模块之间必须是低耦合,如果无法找到一种模型改变方法以实现概念上松耦合,那么久选择整体概念,可以通过有意义的方式将元素组合在一起,基于独立理解和逻辑推理的概念意义上寻求低耦合(而不是强制),优化模型,直到根据高级域概念进行分区,并且相应的代码也被解耦。

(又名打包)

#模块化  #微服务 


聚合

  很难保证具有复杂关联的模型中对象更改的一致性,对象应该保持自己的内部一致状态,但是它们可能会被概念上构成部分的其他对象的变化所遮蔽;谨慎的数据库锁定方案会导致多个用户无意义地相互干扰,并使系统无法使用,在多个服务器之间分配对象或设计异步事务时会出现类似问题。(banq注:聚合是为高一致性的强事务而设计的,不可能所有实体之间操作都是强事务,所以使用通用的事务组件中间件比如JTA等其实是一种试图用技术解决业务的缘木求鱼办法,大概只适合那些不愿意、或者无法进行业务详细分析设计的场合。)

因此:

  将实体和值对象聚类为聚合并定义每个大对象的边界。选择一个实体作为每个聚合的根,并允许外部对象仅保留对根的引用(对内部成员的引用仅在单个操作中使用)。定义整个聚合的属性和不变量,并对根或某些指定的框架机制赋予执行责任。

  使用相同的聚合边界来管理事务和分发。

  在聚合边界内,同步应用一致性规则;跨越边界,异步处理更新。

  将聚合保存在一台服务器上;允许在节点之间分配不同的聚合。

  当设计决策没有根据聚合边界进行引导设计时,重新考虑模型。领域情景是否暗示了一个重要的新见解?这些变化通常会提高模型的表现力和灵活性,以及​​解决事务和分发问题。

#DDD聚合

 

存储库

  以无处不在的语言查询访问聚合的表达。

  可遍历关联的扩散(根据外键一个接一个的查询)仅用于发现混乱模型;在成熟模型中,查询通常表达领域概念,然而查询可能会导致问题。

  使用大多数数据库等基础架构技术会导致复杂性,这种复杂性会弥漫在数据库的调用代码中,从而导致开发人员对领域层进行愚蠢的处理,这使得模型变得无关紧要,没有模型好像也没用事情。

  查询框架可以封装大部分技术复杂性,使开发人员能够以更自动化或声明性的方式从数据库中提取他们需要的确切数据,但这只能解决部分问题。

  无约束的查询可能会从对象中提取特定字段,破坏封装,或者从聚合内部实例化一些特定对象,使聚合根结构不明显,并使这些对象无法强制执行域模型的规则。领域逻辑转移到查询和应用程序层代码中,实体和值对象变成仅仅是数据容器(banq注:变成数据传输对象DTO)。

因此:

  对于需要全局访问的每种类型的聚合,创建一个服务(仓储服务),该服务可以提供该聚合根类型的所有对象的内存中集合的错觉(好像是对象的仓库)。通过众所周知的全局界面设置访问权限,提供添加和删除对象的方法,这些方法将封装数据存储中的实际数据插入或删除。提供基于对领域专家有意义的标准选择对象的方法,返回完全实例化的对象或属性值满足条件的对象集合,从而封装实际的存储和查询技术,或返回以惰性方式给出完全实例化聚合的假象的代理(例如JPA的惰性加载)。这些仅为实际需要直接访问的聚合根提供存储库(banq注:聚合根类似一个物品,物品保存在仓库中,这是存储库的意思,存储库=存储仓库),保持应用程序逻辑专注于模型,

#DDD仓储Repository模式


工厂

  当创建整个内部一致聚合或大值对象变得复杂或显示太多内部结构时,工厂提供封装。

  创建对象本身可能是一项主要操作,但复杂的装配操作不适合所创建对象的责任,将这些责任结合起来可能会产生难以理解的设计。如果使用客户端直接构建会混淆了客户端本身的设计,破坏了组装对象或聚合的封装,并且过度地将客户端耦合到所创建对象的实现。(banq注:参考Gof工厂模式)

因此:

  将创建复杂对象和聚合实例的责任转移到单独的工厂对象,该对象本身在领域模型中不承担任何其他责任,但仍然是领域设计的一部分。提供一个封装所有复杂程序集的接口,并且不需要客户端引用要实例化的对象的具体类。创建整个聚合作为一个原子单位(banq注:类似ACID中的A原子性),强制执行其不变量。将一个复杂的值对象的创建也作为一个原子单位创建,然后使用Builder模式进行组装。
(banq注:工厂和仓储可见:JiveJdon案例,在Spring-data-jdbc或JPA中,由Spring-data框架提供工厂和框架:)

第三章:灵活性设计

领域驱动设计参考