【领域驱动设计(二)】DDD之模型驱动设计的构造块

领域驱动设计系列文章目录

第一篇:运用领域模型
第二篇:模型驱动设计的构造块



在这里插入图片描述

前言

  
  为了保证软件实现的简洁且与模型保持一致,不管实际情况如何复杂,必须运用建模和设计的最佳实践。领域驱动设计不是面向对象,也不是设计原理,他改变了某些传统观念的侧重点。
  
  领域驱动设计遵循 “职责驱动设计” 的原则,但当项目遇到困难时,开发人员可能发现这些原则无法适用于项目当前的情况。为了是领域驱动设计更加灵活,开发人员需要理解这些原则来支持 MODEL-DRIVEN DESIGN,这样才能在设计中做一些折中选择,而又不脱离正确的轨道。


一、分离领域

  
LAYERED ARCHITECTURE(分层架构):

  1. 用户界面层(表现层):负责向用户显示解释用户命令,也就是对外提供 API。
  2. 应用层:定义软件要完成的任务,并且指挥协调领域对象进行不同的操作。该层不包含业务领域知识,只是对领域层的业务做编排。
  3. 领域层(模型层):负责表达业务概念,业务状态信息以及业务规则。该层是业务软件的核心。
  4. 基础设施层:为上层提供通用的能力,比如数据库连接或基础功能服务。

  给复杂的程序划分层次,在每一层内分别进行设计,使其具有内聚性并依赖于它的下层,采用标准的架构模式,只与上层进行松散耦合。将领域模型相关的代码放在一层,并把它与用户界面层、应用层和基础设施层的代码分开。领域对象应该把重点放在如何表达领域模型上,不需要考虑自己的显示和存储问题,也无需管理应用任务等内容。这种分层可以是每层的设计更加清晰,彼此独立便于维护,因此可以使得他们以不同的速度发展来满足不同的需求。

在这里插入图片描述
  大部分软件都采用分层架构,只是分层的方案不同,在领域驱动设计里,领域层是模型的精髓,它是模型设计元素的表现,也是模型概念的映射。
  

二、软件中所表示的模型

  

1、ENTITY(实体)

  主要由标识定义的对象被称作 ENTITY。它们具有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。为了有效的跟踪这些对象,必须定义它们的标识。而唯一标识和可变性也是 ENTITY 和 VO 的主要区别。所以我们会把它和数据库表一一映射。

2、VO(Value Object)

  VO 不关注标识,只关注属性。它可以是多个对象的集合,甚至可以引入 ENTITY。所以我们经常把它作为参数在对象之间传递消息。

3、SERVICE(服务)

  有时候,对象不是一个事物,有些重要的领域操作(活动或动作)无法放到 ENTITY 或 VO 中,这就需要引入 SERVICE 的概念。它是作为接口提供的一种操作,强调的是和其他对象的关系,与 ENTITY 和 VO 不同,它只是定义了客户行为。所以它常常以一种活动来命名,而不是根据 ENTITY 来命名。(以往我们都是 SERVICE 和 ENTITY 相对应,不过这也导致很多领域行为交织在一起,所以这块落地还挺不容易的。)

4、MODULE(也叫PACKAGE)

  MODULE 也就是我们所说的包,它是一种比较传统和成熟的元素。为我们提供了两种观察模式:一是可以进入到 MODULE 中查看细节,而不被整个淹没淹没;二是可以查看 MODULE 之间的关系,而不用考虑其细节。而领域层的 MODULE 意义最为重要,因为它从更大的角度描述了领域。

5、建模范式

5.1、对象范式

  一些团队选择对象范式并不是出于技术原因,甚至也不是出于对象本身的原因。而是在简单性和复杂性之间平衡后的一个选择结果。一个建模范式过于深奥,那么大多数人员无法掌握它,因此也无法正确的运用它。而如果团队中的非技术人员无法掌握这种范式的基本知识,那么他们将无法理解模型。虽然对象建模的概念很简单,但是它足以用来捕获和表达重要的领域知识。而且有丰富的成熟的工具支撑,使得模型可以更好的在软件中表达出来。

  开发者社区和设计文化的成熟也很重要,采用新范式,可能很难找到精通它的开发人员,也很难找到能够使用新范式创建有效模型的设计人员。成熟的对象建模技术,业内已经形成了很多现成的解决方案,它们可以满足大部分常见的基础设施需要,多数大型供应商,或者稳定的开源项目都提供了关键工具。这些基础设施本身已经被广泛应用,因此了解它们的人很多。

5.2、非对象范式

  领域模型不一定是对象模型,有些模型是由逻辑规则和事实组成。不管在项目中使用何种范式,领域中都可能会有一些部分更容易用另外的范式来表达。因此当领域中的主要部分明显属于不同范式时,明智地选择是用适合各个部分的范式对其建模,并使用混合工具来实现。

5.3、混合范式

  建模未必一定是面向对象技术,它需要的是一种富有表达力的模型结构实现,无论是对象、规则还是工作流。当使用非对象元素混合面向对象技术为主的系统时,需要遵循下面几条经验原则:

  1. 不要和实现范式对抗,我们总是可以用别的方式来考虑领域,找到适合于范式的模型概念。
  2. 把通用语言作为交流的基础,并用它讨论模型。
  3. 不要一味依赖UML,UML确实有一些特性很适合表达约束,但并不是在所有情况下都适用,有时候其它风格的图形(可能适用于其它范式)或者简单的语言描述比牵强地使用某种对象视图表达力更强。
  4. 保持怀疑态度,不要滥用混合范式,要问下自己是不是不得不用混合范式,在使用混合范式前,确保各种可能性都已经尝试过了。如果可以,还是使用统一的对象建模更能保持模型的统一性,哪怕稍微显得不是那么优雅。
      

三、领域对象的生命周期

  
  每个对象都有生命周期,对象自创建后,可能会经历各种不同的状态,直至最终消亡——要么存档,要么删除。当然很多对象是简单的临时对象,仅通过调用构造函数来创建,用来做一些计算,然后由垃圾收集器回收。这类对象没必要搞得那么复杂。但有些对象具有更长的生命周期,其中一部分时间不是在活动内存中度过的。它们与其他对象具有复杂的相互依赖性。它们会经历一些状态变化,在变化时要遵守一些固定规则。管理这些对象时面临诸多挑战,稍有不慎就会偏离 MODEL-DRIVEN DESIGN 的轨道。
  
在这里插入图片描述
  主要的挑战有两类:一是在整个生命周期中维护完整性,二是防止模型陷入管理生命周期复杂性造成的困境当中。那么我们将从三种模式去解这个问题。

1、AGGREGATE(聚合)

  减少设计中的关联有助于简化对象之间的遍历,并在某种程度上限制关系的急剧增多。但大多数业务领域中的对象都具有十分复杂的联系,以至于最终形成很长、很深的对象引用路径,我们不得不在这个路径上追踪对象。在某种程度上,这种混乱状态反映了现实世界,因为现实世界中就很少有清晰的边界。但这却是软件设计中的一个重要问题。

  AGGREGATE 就是一组相关对象的集合,我们把它当作数据修改的单元。每个 AGGREGATE 都有一个根(root)和一个边界(boundary)。边界定义了 AGGREGATE 的内部都有什么。根则是 AGGREGATE 所包含的一个特定 ENTITY。对 AGGREGATE 而言,外部对象只可以引用根,而边界内部的对象之间则可以相互引用。除根以外的其他 ENTITY 都有本地标识,但这些标识只在 AGGREGATE 内部才需要加以区别,因为外部对象除了根 ENTITY 之外看不到其他对象。

  固定规则(invariant)是指在数据变化时必须保持的一致性规则,其涉及 AGGREGATE 成员之间的内部关系。而任何跨越 AGGREGATE 的规则将不要求每时每刻都保持最新状态。通过事件处理、批处理或其他更新机制,这些依赖会在一定时间内得以解决。但在每个事务完成时,AGGREGATE 内部所应用的固定规则必须得到满足。

  只有 AGGREGATE 的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现。AGGREGATE 内部的对象可以保持对其他 AGGREGATE 根的引用。删除操作必须一次删除 AGGREGATE 边界内的所有对象(利用垃圾收集机制,这很容易做到。由于除根以外的其他对象都没有外部引用,因此删除了根以后,其他对象均会被回收)。当提交对 AGGREGATE 边界内部的任何对象的修改时,整个 AGGREGATE 的所有固定规则必须被满足。

2、FACTORY(工厂)

  FACTORY 是我们非常熟悉的一种设计模式了,当创建一个对象或创建整个 AGGREGATE 时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用 FACTORY 进行封装。

  当客户创建对象时,它会牵涉不必要的复杂性,并将其职责搞得模糊不清。这违背了领域对象及创建的 AGGREGATE 的封装要求。更严重的是,如果客户是应用层的一部分,那么职责就会从领域层泄漏到应用层。应用层与实现细节之间的这种耦合使得领域层抽象的大部分优势荡然无存,而且导致后续更改的代价变得更加高昂。

  对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或 AGGREGATE 的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。

  复杂的对象创建是领域层的职责,应该将创建复杂对象的实例和 AGGREGATE 的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建 AGGREGATE 时要把它作为一个整体,并确保它满足固定规则。

2.1、选择 FACTORY 及其应用位置

  FACTORY 与被构建对象之间是紧密耦合的,因此 FACTORY 应该只被关联到与被构建对象有着密切联系的对象上。当有些细节需要隐藏(无论要隐藏的具体实现还是构造的复杂性)而又找不到适合的地方来隐藏它们时,必须创建一个专用的 FACTORY 对象或 SERVICE。整个 AGGREGATE 通常由一个独立的 FACTORY 来创建,FACTORY 负责把对根的引用传递出去,并确保创建出的 AGGREGATE 满足固定规则。如果 AGGREGATE 内部的某个对象需要一个 FACTORY ,而这个 FACTORY 又不适合在 AGGREGATE 根上创建,那么应该构建一个独立的 FACTORY。但仍应遵守规则——把访问限制在 AGGREGATE 内部,并确保从 AGGREGATE 外部只能对被构建对象进行临时引用。

2.2、有些情况下只需使用构造函数

  FACTORY 的引入提供了巨大的优势,而这种优势往往并未得到充分利用。但是,在有些情况下直接使用构造函数确实是最佳选择。FACTORY 实际上会使那些不具有多态性的简单对象复杂化。

  在以下情况下最好使用简单的、公共的构造函数。

  1. 类(class)是一种类型(type)。它不是任何相关层次结构的一部分,而且也没有通过接口实现多态。
  2. 客户关心的是实现,可能是将其作为选择策略的一种方式。
  3. 客户可以访问对象的所有属性,因此向客户公开的构造函数中没有嵌套的对象创建。
  4. 构造并不复杂。
  5. 公共构造函数必须遵守与 FACTORY 相同的规则:它必须是原子操作,而且要满足被创建对象的所有固定规则。

  不要在构造函数中调用其他类的构造函数。构造函数应该保持绝对简单。复杂的装配,特别是 AGGREGATE ,需要使用 FACTORY。

2.3、固定规则的相关逻辑应放在哪里

  FACTORY 可以将固定规则的检查工作委派给被创建的对象,但在某些情况下,把固定规则的相关逻辑放到 FACTORY 中是有好处的,这样可以让被创建对象的职责更明晰。在实际的设计和开发过程中,需要根据项目甚至具体的业务情况做抉择。

2.4、重建已存储的对象

  用于重建对象的 FACTORY 与用于创建对象的 FACTORY 很类似,主要有以下两点不同。

  1. 用于重建对象的 ENTITY FACTORY 不分配新的跟踪 ID。如果重新分配 ID,将丢失与先前对象的连续性。因此,在重建对象的 FACTORY 中,标识属性必须是输入参数的一部分。
  2. 当固定规则未被满足时,重建对象的 FACTORY 采用不同的方式进行处理。当创建新对象时,如果未满足固定规则,FACTORY 应该简单地拒绝创建对象,但在重建对象时则需要更灵活的响应。如果对象已经在系统的某个地方存在(如在数据库中),那么不能忽略这个事实。但是,同样也不能任凭规则被破坏。必须通过某种策略来修复这种不一致的情况,这使得重建对象比创建对象更困难。

3、REPOSITORY(存储库)

3.1、DDD中的REPOSITORY

  REPOSITORY 也是我们非常熟悉的一种设计模式,它是将某种类型的所有对象表示为一个概念集合,它的行为类似于集合,只是具有更复杂的查询功能。在添加或删除相应对象的对象时,REPOSITORY 的后台机制负责将对象添加到数据库中,或从数据库中删除对象。这个定义将紧密相关的职责集中在一起,这些职责提供了对 AGGREGATE 根的整个生命周期的全程访问。

  在所有持久化对象中,有一小部分必须通过基于对象属性的搜索来全局访问。当很难通过遍历方式来访问这些 AGGREGATE 根的时候,就需要使用这种方式。其他对象不宜使用这种访问方式,因为这会混淆他们之间的重要区别。随意的数据库查询会破坏领域对象的封装和聚合。基础设施和数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的概念。

3.2、REPOSITORY与FACTORY的关系

  FACTORY 负责处理对象生命周期的开始,而 REPOSITORY 帮助管理生命周期的中间和结束。当对象驻留在内存中或存储在对象数据库时,这是很好理解的。但通常至少有一部分对象存储在关系数据库、文件或其他非面向对象的系统中。在这些情况下,检索出来的数据必须被重建为对象形式。

  由于在这种情况下 REPOSITORY 基于数据来创建对象,因此很多人认为 REPOSITORY 就是 FACTORY,而从技术角度来看的确如此。但是我们最好还是从模型的角度来看待这一问题,重建一个已存储的对象并不是创建一个新的概念对象。从领域驱动设计的角度来看,FACTORY 和 REPOSITORY 具有完全不同的职责。FACTORY 负责制造新的对象,而 REPOSITORY 负责查找新的对象。REPOSITORY 应该让客户感觉到那些对象就好像驻留在内存中一样。

  职责上的明确区分有助于 FACTORY 摆脱所有持久化职责。FACTORY 的工作是用数据来实例化一个可能很复杂的对象。如果产品是一个新对象,那么客户将知道创建完成之后应该把它添加到 REPOSITORY 中,由 REPOSITORY 来封装对象在数据库中的存储。

在这里插入图片描述

3.2、REPOSITORY与DAO的区别

  很多人会混淆这两个概念,包括我也一样,今天我们来总结一下它们的区别:

  1. DAO 是数据持久性的抽象;REPOSITORY 是对象集合的抽象。
  2. DAO 是一个较低级别的概念,更接近存储系统;REPOSITORY 是一个更高层次的概念,更接近领域对象。
  3. DAO 作为数据映射/访问层工作,隐藏丑陋的查询;REPOSITORY 是领域和数据访问层之间的层,隐藏了整理数据和准备领域对象的复杂性。
  4. DAO 不能使用存储库实现;REPOSITORY 可以使用 DAO 来访问底层存储。

  如果我们有一个贫乏的领域,存储库将只是一个 DAO。REPOSITORY 模式鼓励域驱动设计,也为非技术团队成员提供了对数据结构的轻松理解。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

砍光二叉树

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值