DDD领域驱动模型

什么是领域?什么是领域模型?

没有丰富的领域知识能做出复杂的银行业业务软件吗 ? 没门 。 答案永远是否定的 。 那么谁 了解银行业业务 ? 软件架构师吗 ? 不 , 他只是在使用银行来保护他的财产安全 , 并且确保需要 钱的时候能够取出来 ; 软件分析师吗 ? 也不是 , 他只懂得在已获取到所有材料的情况下 , 对一 个给定的主题进行分析 ; 软件开发人员 ? 别难为他了 。 那么还有谁 ? 当然是银行的从业者了 。 银行业务系统被银行的内部人员和专家所熟知 。 他们知道所有的细节 、 所有的困难 、 所有可能出现的问题 、 所有的业务规则 。 这些就是我们永远的起始点 : 领域

  • 软件的最终目的是增进一个特定的领域 。 为了达到这个目的 , 软件需要跟要它服务的领域和谐相处 , 否则 , 它就会给领域引入束缚 , 引起故障 、 造成破坏甚至导致很大的混乱 。
  • 领域是真实世界中某些事物,软件需要为该领域创建一个抽象。
  • 软件需要包含领域里重要的核心概念和元素 , 并精确实现它们之间的关系 。也就是说 , 软件需要对领域进行建模 ,创建 一个关于领域的模型

领域模型的特点

  1. 汇集业务专家、开发人员、用户的知识碰撞,它是一个蓝图 。 开始时总是不完整的 , 但随着各方的努力它越来越清晰完善
  2. 模型是我们对目标领域的内部展现方式,我们对领域的所有的思考过程 被汇总到这个模型中
  3. 模型是软件的根本 , 但我们需要找到一些方法来表达它 , 我们需要用模型来交流
  4. 精确的模型提炼能让程序更加容易实现和理解

传统系统设计方法

瀑布设计方法——

  1. 业务专家提出一堆需求同业务分析人员进行交流 ,
  2. 分析人员基于那些需求来创建 模型
  3. 开发人员根据他们收到的 模型 开始编码 。

缺点:业务专家得不到分析人员的反馈信息 , 分析人员也得不到开发人员的反馈信息 。知识只有单一的流向 。

敏捷方法学——

针对难预先确定所有的需求 , 特别是在需求经常变化的情况。核心思想就是当前有什么需求就做对应的功能,不做预先设计,通过由业务涉众持续参与的迭代开发和 很多重构 , 开发团队更多地学习到了客户的领域知识 , 从而能够产出满足客户需要的软件 。

缺点:他们提倡简单 , 但每个人都对 “ 简单 ” 的 含义 有着自己的观点 。 同时 , 缺乏了真实可见的设计原则 , 由开发人员 完成的 持续重构会导致代码更难理解或者 更难改变 。而且会产生大量的过度工程

领域驱动设计——

在任何开发过程中 应用这些原则 , 开发团队以一种可维护的方式 对领域内复杂问题进行建模和实现的能力 , 都将会得到极大提升 。

领域驱动设计结合了设计和开发实践 , 展示了设计和开发如何协同工作以创建一个更好的解决方案

优良的设计会加速开发的过程 , 而开发过程中的反馈也会进一步优化设计 。

建模的注意点

  • 我们需要对模型包含的信息进行取舍(银行业软件肯定会跟踪客户的姓名电话 , 但它决不会关心客户的眼睛是什么颜色的)

语言交流

  1. 开发和业务专家充分沟通后通过建模语言来沟通(UML)
  2. 设计文档的最大价值是解释模型概念

软件架构师、开发人员和领域专家组成的设计团队,需要有一种语言来统一它们的行动,以帮助它们创建一个模型,并使用代码来表达模型.

模型驱动设计

  • 根本的问题——我们应该如何完成从模型到代码的转换 。
  • 选择一个能够被轻易和准确地转换成代码的模型是很重要的 。

系统结构分成

层级 介绍
用户界面/展现层 负责向用户展现信息以及解释用户命令
应用层 很薄的一层 ,用来协调应用的活动 。它不包含业务逻辑。它也不保留业务对象的状态 ,但它保 留有应用任务的进度状态
领域层 本层包含关于领域的信息 。这是业务软件的核心所在 。在这里保留业务对象的状态 。对业务对象和它们状态的持久化被委托给了基础设施层
基础设施层 本层作为其他层的支撑库存在 。它提供了层间的通信 ,实现对业务对象的持久化 ,包含对用户界面层的支持库等作用

实体

  • 是一类对象。
  • 拥有标识符,它的标识符在历经软件的各种状态变更后仍能保持一致
  • 对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至能够超出软件系统的生命周期。
    比如“天气”这个对象,温度、风级等属性组成,如果里面的属性值一样两个对象之间可以相互替换,这种所有属性赋值后不能唯一标识它为唯一对象的就不能称之为实体。
    比如定义一个“人”这个对象,设置姓名、年龄、身高、户籍所在地几个属性,这样的“人”也不能称之为实体,因为同一个地域可能存在同龄同身高同名的人。这个对象缺乏一个属性能唯一标识这个对象是唯一的。如果加入“身份证号”这个属性,即可标识出唯一性,即可成为实体。
    实体——有唯一标识属性
    通常标识符或是对象的一个属性(或属性的组合),一个专门为保存和表达标识符而创建的属性,甚至可以是一种行为。
    原则(什么时候需要实体)
    1. 系统能够轻易区分开两个拥有不同标识符的对象
    2. 两个使用了相同标识符的对象看做是相同的
    3. 类的定义保持简单并专注于生命周期的延续性
      比如我想画画,创建一个Point的类,属性为相对画布或者画板的坐标,有没有必要给这个类设定一个唯一标识符?这个标识符有连续性吗?(我们会给一个x=3,y=2的点定义一个唯一标识ID = 1,然后画画的时候说从一号点画一条直线到二号点吗?下次还会这样使用这个唯一标识吗?如果不使用那这个唯一的标识就没有连续性。因为这个类重要的是坐标而已,所以没有必要设计成实体。而且一个画布有大量的点,去一个个创建唯一ID,然后追踪使用这个ID,是需要系统开销的,并且没有必要)

标识符ID的生产方式

  1. 模块自动生成,仅在软件内部使用
  2. 数据库的自增主键
  3. ID也可能是用户创建的,比如机场的编号(机场的编号的命名全球使用同一套命名规则)

实体是领域模型中非常重要的对象,并且它们应该在建模过程开始时就被考虑。
决定一个对象是否需要成为一个实体也很重要。
实体是可以被跟踪的,但跟踪和创建标识符需要很大的成本。

值对象

用来描述领域的特定方面、并且没有标识符的一个对象,叫做值对象。

选取规则

  • 只建议选择那些符合实体定义的对象作为实体,而将剩下的对象实现为值对象

值对象的意义

  • 没有标识符,值对象就可以被轻易地创建或者丢弃。
  • 没有人关心创建一个标识符,在没有其他对象引用时,垃圾回收会处理这个对象。这极大简化了设计。

值对象设计规则

  • 极力推荐将值对象实现为不可变的,如果想得到这个对象不同值得时候,简单得创建另外一个对象就行。
  • 实现为不可变的,并且不具有标识符,值对象就能够被共享了。
  • 在性能很重要的场合,不可变的对象是可以共享的。它们也能维持一致性
  • 如果值对象是可共享的,那么它们应该是不可变的。
  • 值对象应该保持很小、很简单。
  • 在多方需要值对象时,可以简单地传递值,或者创建一个副
    本。

设计成不可变得对象得意义:
设想一下共享一个可变的对象会意味着什么。
航班设计成共享对象。
一个航空旅行预定系统能够为每个航班创建对象,假设:
顾客甲预定可一个航班A,(订单里包含了一个航班A对象)
顾客乙也同样的选择了航班A(订单里包含了一个航班A对象)
假设航班对象可变,当顾客甲改签航班B,如果航班A修改成B,那乙的航班也变成了B,这显然是有问题的

注意的点

  • 值对象可以包含其他的值对象 ,它们甚至还可以包含对实体对象的引用 。
  • 被选择用来构成一个值对象的属性应该形成一个概念上的整体。比如:
    • 淘宝用户:账号、昵称、电话、城市、街道、小区
    • 应该拆分为
      • 淘宝用户:账号、昵称、电话、地址
      • 地址:城市、街道、小区

服务

开发通用性语言,领域中的关键概念被引入到语言中,语言中的

  • 名词——被映射成对象
  • 名词的动词——被映射成对象的行为
  • 某些动词——看上去不属于任何一种对象,但却是领域里的一个重要行为(比如转账,放在转入和转出账户对象里好像都不合适)
    这样的行为从领域里被识别出来时,最佳实践就是将它声明成一个服务。放到一个“服务中心”身份的对象里(比如银行、支付宝的角色)。

服务的认知

服务——仅仅为领域提供相应的功能

  • 服务是多个对象的一个连接点
  • 服务解构了对象之间的关系(类似中介模式)
  • 服务不应该替代隶属于领域对象的操作

服务的三个特征

  1. 服务执行的操作代表了一个领域概念 , 这个领域概念无法自然地隶属于一个实体或者值对象 。
  2. 被执行的操作涉及到领域中的其他的对象 。
  3. 操作是无状态的(操作名尽量与通用语言相同,它的变化基本上不影响领域的其他实体和值对象)

创建和使用服务的注意事项

  1. 很容易弄混属于领域层的服务和属于基础设施层的服务
  2. 应用层也可能有服务,应用层是很薄的一层 ,它位于用户界面层与领域层 、
    基础设施层之间 。
  3. 注意各个层级的隔离 如果所执行的操作概念上属于应用层,那么服务就应该放到应用层,如果操作是关于领域对象的,为领域服务的,那它就属于领域层。

模块

模块是一种提高内聚性和消除耦合度的方法

给定的模块名称会成为通用语言的组成部分。模块和它们的名称应该能够反映出对领域的深层理解。

  • 模块被用来作为组织相关概念和任务以便降低复杂性的一种方法。
  • 理解了模块之间的交互之后,人们就可以开始处理模块中的细节了。,这是管理复杂性的简单有效的方法。
  • 软件代码应该具有高层次的内聚性和低层次的耦合度。虽然内聚开始于类和方法级别,它也可以应用于模块级别

模块的使用方式

将高关联度的类分组到一个模块,以提供尽可能大的内聚性

具体模块类型

  • 通信性内聚——模块中的部件操作相同的数据
  • 功能性内聚——模块中的部件协同工作以
    完成定义好的任务
    功能性内聚被认为是最佳的内聚类型

模块应该由在功能上或者逻辑上属于一体的元素构成,以确保内聚性

与其他模块的通信

  1. 模块应该具有定义好的接口,这些接口可以被其他的模块访问。
  2. 模块最好用访问一个接口的方式访问另外其他模块,而不是调用其他模块里的几个对象(可以降低耦合度)
  3. 尽量减少模块之间的通信连接,这样系统的层级更加分明更容易理解,模块和它们的名称能反映出对领域深层理解。

模块设计的注意事项

  • 模块的设计要灵活,允许模块随着项目发展而进化,并且不应该被冻结。
  • 模块的重构成本比类的重构成本高得多

管理领域对象的生命周期

领域对象在它们的生命周期内会历经若干种状态 ,它们被创建 、放在内存中 、在计算中被使用 、直到最后消亡 。有时它们会被保存到一个永久位置 ,例如数据库中 ,这样可以在以后的日子里被获取到 ,或者被存档 。有时它们会被完全从系统中清除掉 ,包括从数据库和归档介质上 。

三大模式

  1. 聚合
  2. 工厂
  3. 资源库

聚合

聚合是一个用来定义对象所有权得和边界 的领域模式

当对象之间存在一对多的关系,当这个“一”的对象被删除,其他“多”的对象包含它的引用也应该被清除。比如一个学生毕业了,那它对应的班级、兴趣协会、学院都应该删除这个学生的引用。同样的一个学生的信息发生变化,那整个系统里含有整个对象引用的其他对象也要做适量更新,达到数据一致性的要求。
这通常是在数据库层面去处理的,通过事务确保数据一致性。但是如果设计不当,会产生很大程度的数据库争夺,导致性能变差。
我们期望直接在模型中解决数据库一致性的问题
在模型中拥有复杂关联对象发生变化时,很难保证其一致性

  1. 聚合是针对数据变化可以考虑成一个单元的一组关联的对象。
  2. 聚合使用边界将内部和外部的对象划分开来。
  3. 每个聚合都有一个根。这个根是一个实体,并且它是外部可以访问的唯一的对象
  4. 根对象可以持有对任意聚合对象的引用,其他的对象可以互相持有彼此的引用,但一个外部对象只能持有对根对象的引用。
    聚合的概念介绍
    聚合的划分

聚合是如何保持数据

一致性和强化不变量的

  1. 其他对象只能持有根对象的引用,而不能直接修改聚合子对象。如果要更改子对象,它们只能通过根对象来执行某些操作。
  2. 根对象能够变更其他的对象,但这是聚合内包含的操作,并且它是可控的
  3. 如果根从内存中被删除或者移除,聚合内的其他所有的对象也将被删除,因为再不会有其他的对象持有它们当中的任何一个了。
  4. 当针对根对象的修改间接影响到聚合内的其他的对象,强化不变量变得简单了,因为根将做这件事情。如果外部对象能直接访问内部对象并且变更它们时,这将变得很难管理这些对象的一致性和不变量性

根对象如何传递内部对象

  1. 根对象将内部对象的临时引用传递给外部对象,作为限制,外部对象在使用完后,不再持有这个内部对象的引用
  2. 向外部对象传递值对象的副本,值对象副本的变化不会引起聚合的一致性
  3. 根对象能够变更其他的对象,但这是聚合内包含的操作,并且它是可控的

聚合与数据的交互

  1. 如果一个聚合中的对象被保存到数据库中,可以通过查询来获得的应该只有根对象。
  2. 其他的对象只能通过从根对象出发导航关联对象来获得。
  3. 聚合内的对象可以被允许持有对其他聚合的根对象的引用。
  4. 根实体拥有全局的标识符,并且有责任维护不变量。内部的实体拥有内部的标识符

步骤:

  1. 将实体和值对象聚集在聚合之中,且定义各个聚合之间的边界。
  2. 为每个聚合选择一个实体作为根,并且通过根来控制所有对边界内的对象的访问。
  3. 允许外部对象仅持有对根的引用。对内部成员的临时引用可以被传递出来,但是仅能用于单个操作之中因为由根对象来进行访问控制,将无法盲目地对内部对象进行变更。这种安排使得强化聚合内对象的不变量变得可行,并且对聚合而言,它在任何状态变更中都是作为一个整体。

聚合的本质就是建立了一个比对象粒度更大的边界,聚集那些紧密关联的对象,形成了一个业务上的对象整体。
使用聚合根作为对外的交互入口,从而保证了多个互相关联的对象的一致性

使用聚合的好处

  1. 更容易地保证业务规则的一致性
  2. 减少了对象之间可能的耦合
  3. 提升设计的可理解性
  4. 降低出问题的可能性。

聚合划分的原则

应该遵循高内聚、低耦合的原则,聚合边界内的对象应该满足如下的启发式原则:

  • 生命周期一致性
  • 问题域一致性
  • 场景频率一致性
  • 聚合内的元素尽可能少

生命周期一致性

如果聚合根消失,聚合内的其他元素都应该同时消失。

可以用反证法来证明生命周期一致性:如果一个对象在聚合根消失之后仍然有意义,那么说明在系统中必然需要存在其他方法访问该对象。这和聚合的定义相矛盾。所以聚合根内的其他元素必然在聚合根消失后失效。

对于那些说不清楚是否应该划入同一个聚合的对象,不妨问一下:这个对象如果离开本聚合的上下文,是否还有单独存在的价值?如果答案是肯定的,该对象就不应该划到本聚合中

问题域一致性

问题域一致是限界上下文(Bounded Context)的约束
”一个对象脱离另外一个对象是否有存在的意义“
一个在线论坛,用户可以对论坛上用户的文章发表评论。文章显然应该是一个聚合根。如果文章被删除,那么,用户的评论看起来也要同时消失。那么评论是否可以属于文章这个聚合?

让我们来考虑评论是否还可能有其他的用途。例如,一个图书网站,用户可以对图书发表评论。如果只是因为文章删除和评论删除之间存在逻辑上的关联,就让文章聚合持有评论对象,那么显然就约束了评论的适用范围

一目了然的事实是,评论这一个概念,在本质上和文章这个概念相去甚远。所以,我们得到了一个新的、凌驾于原则 1 之上的原则:
不属于同一个问题域的对象,不应该出现在同一个聚合中。
由于聚合根无法保证聚合之外的一致性,所以我们需要依赖”最终一致性“来实现聚合之间的一致性。
例如,在文章删除的时候,发送一个文章删除的消息。评论系统接收到文章删除消息之后,删除文章对应的评论。

场景频率一致性

依赖于前述两个原则已经能够区分出大多数聚合。但是,仍然会存在一些比较复杂的情况。例如,考虑软件开发中的“产品”和“版本”以及“功能”的关系。“产品”和“版本”算不算是同一个问题域?——这几个概念之间的关系可能就不如“文章”和“评论”那么清晰。不过不要紧,我们仍然有一个启发式规则来规避这种模糊性。这就是“场景频率一致性”原则。

场景操作频率的一致性是同一聚合内部对象的一个关键表征。
经常被同时操作的对象,它们往往属于同一个聚合。而那些极少被同时关注的对象,一般不应该划为一个聚合。

“产品”、“版本”和“功能”这三个概念为例来说明。产品确实包含了很多功能,这些功能通过一系列的版本发布。但是,在产品层面的操作,例如查看所有的产品列表,却并不需要关心特定功能的详细信息,也不需要了解特定的某个版本信息。我们做版本规划的时候,确实会用到功能列表,但是大多数时候我们并不会去查看功能详情,更加不可能在做版本规划的时候修改功能描述。

按场景一致性划分
  1. 不在同一个场景下操作的对象,放入同一个聚合意味着每次操作一个对象,就需要把其他对象的所有信息抓取到,这是非常没有意义的。
  2. 从实现层次,如果不紧密相关的对象出现在同一个聚合中,会导致它们经常在不同的场景中被并发修改,也增加了这些对象之间冲突的可能性。

结论:
操作场景不一致的对象,或者说如果一个对象在不同场景下都会被使用,应该考虑把它们分到不同的聚合中。

尽量小的聚合

聚合出现的本质是解决一致性问题带来的复杂性。因此,那么凡是不破坏以上三个一致性的情况,都没有必要把它们放到同一个聚合中。仅仅由一个业务概念(即领域模型中的类名及属性以及后面马上提到的 Id 对象)构成的聚合在面向对象的世界中是大多数。

工厂

实体和聚合常常会很大很复杂,过于复杂以至于难以通过根实体的构造器来创建。通过根实体的构造器来构建复杂的聚合,看上去就像是要用打印机构建打印机本身。
当对象构建是一个很费力的过程时 ,创建这个对象会涉及到大量的知识 (例如对象的内部结构 、所包含对象之间关系的以及应用在这些对象上的规则)等 。
这意味着该对象的每个客户将持有关于该对象构建的专用知识 。在真实生活中,
这就像是给我们塑胶、橡胶、金属、硅,让我们来构建自己的打印机?这不是不可能完成的,但这样做值得吗?

创建一个对象可以是它自身的主要操作,
但是复杂的组装操作不应该成为被创建对
象的职责。

组合这样的职责会产生笨拙的设计,也很难让人理解。

定义:

有必要引入一个新的概念,这个概念可以帮助封装复杂的对象创建过程,它就是工厂(Factory)。

工厂的特点

  1. 工厂被用来封装对象创建所必需的知识 ,它们对创建聚合特别有用 。
  2. 聚合的根被创建后 ,所有聚合包含的对象将随之创建 ,所有的不变量得到了强化 。

对象创建的注意事项

  1. 保持创建过程的原子性,确保在创建过程里不会中断,聚合内对象服从的不变量初始化成有效状态。
  2. 为复杂对象和聚合创建实例的职责,应该转交给一个单独的对象。提供一个接口来封装所有复杂的组装过程,客户不需要引用正在初始化的对象所对应的具体类。
  3. 将整个聚合当作一个单元来创建,强化它们的不变量

实现工厂的设计模式

主要有工厂方法(Factory Method) 和抽象工厂(Abstract Factory)。
方式
给聚合的根添加一个方法 ,由这个方法来负责对象的创建,强化所有的不变量,返回对那个对象的一个引用或者一个副本。

注意事项

  1. 当创建一个工厂时,我们被迫违反一个对象的封装原则。每当对象中发生了某种变化时,需要确保工厂也被更新以支持新的条件。
  2. 根的构建与聚合内的其他对象的创建都依赖工厂类,个工厂类中将包含应该为聚合强化的规则、约束和不变量。尽量保证简单。

工厂分类

  • 值对象工厂——值对象通常是不可变的对象,并且其所有必需的属性需要在创建时完成。当一个对象被创建之后,它必须是有效的,也是最终的,不会再发生变化。
  • 实体工厂——实体对象并非是不可变的。在被创建以后,它们可以通过设置某些属性。实体对象需要标识符(唯一主键)

工厂和构造器的选择

以下情况选择构造器:

  • 构造过程并不复杂。
  • 一个对象的创建不涉及到其他对象的创建,可以将所有需要的属性传递给构造器。
  • 客户对实现很感兴趣,可能希望选择使用策略(Strategy) 模式 。
  • 类是特定的类型,不存在到层级,所以不用在一系列的具体实现中进行选择 。

需要工厂创建的场景:

  • 从无到有创建一个新对象 ,也或者它们需要对先前已经存在但可能已经持久化到数据库中的对象进行重建。
  • 复杂对象的构建

注意事项

  • 将实体对象从它们所在的数据库中取回内存中,包含的是一个与创建一个新对象完全不同的过程
  • 重建的新对象不需要一个新的标识,这个对象已经有一个标示符了,对不变量的违反也将区别对待。

资源库(repository)

须知

  • 在模型驱动设计中,对象有一个生命周期,从被创建开始,直到被删除或者被归档结束
  • 可以使用一个构造器或者工厂来负责对象的创建,创建对象的目的完全是为了使用
  • 我们必须保持对一个对象的引用才能够使用它 。客户必须创建一个对象或者通过导航已有的关联关系从另一个对象中获得它
    要使用一个对象,则意味着这个对象已经被创建完毕了。
  • 如果这个对象是聚合的根,那么它是一个实体,它会被保存为一个持久化的状态,可能是在数据库中,也可能是其他的持久化形式。
  • 如果它是一个值对象,可以通过导航一个关联关系从一个实体中获得它。

数据库的设计

  • 一个糟糕的解决方案是客户程序必须知道访问数据库所需的细节。

例如,客户需要创建SQL 查询语句来检索想要的数据 。 数据库查询可能会返回一组记录 ,甚至会暴露出其内部更多的细节 。当许多客户程序不得不直接从数据库创建对象时 ,会导致这样的代码扩散到整个模型中 。从这点上讲领域模型遭受了损害 。模型会处理大量基础设施的细节而不是处理领域概念。

客户程序直接访问数据库的危害

  • 当客户代码直接访问一个数据库时,它极有可能会恢复聚合内部的一个对象。这样做会破坏聚合的封装性,带来未知的结果。
  • 客户代码直接访问数据库,使得领域逻辑分散到查询和客户代码中,实体和值对象变得仅仅是数据的容器,领域层的重要性降低了,所作的工作脱离了模型的概念,丢失了对领域的专注,设计受到损害。
    资源库的作用—— 封装所有获取对象引用所需的逻辑

领域对象
不需处理基础设施,以得到领域中对其他对象的引用。只需要从资源库中获取它们,于是模型重获它应有的清晰和专注

资源库的特点

  • 资源库会保存对某些对象的引用
  • 当一个对象被创建之后,它可以被保存到资源库中,可以从资源中获取到以备后续使用。
  • 客户程序从资源库中请求一个对象,而资源库中不存在,资源库就会从存储介质中获取它。

总结:资源库扮演了一个全局可访问对象的存储地点。是领域模型本身与需要保存对象或它们的引用、访问底层持久化基础设施实现了解耦。

对外提供的具体功能

  1. 过一个众所周知的全局接口来设置值对象或实体的访问途径
  2. 提供方法来添加或者删除对象,封装向数据存储中插入或者删除数据的实际操作
  3. 提供基于某些条件选择对象的方法,返回属性值符合条件的完全实例化的对象或对象集合,从而封装实际的存储和查询技术
  4. 仅仅为真正需要直接访问的聚合根提供资源库
    总结 让客户程序保持对模型的专注,将所有的对象存储和访问细节都委托给资源库
    资源库的实现可能会非常像是基础设施,然而资源库的接口却是纯粹的领域模型

工厂和资源库的异同

相同点:
  • 它们都是模型驱动设计中的模式,管理领域对象的生命周期
差异:
  • 工厂关注的是对象的创建(用来创建新的对象)
  • 资源库关注的是已存在的对象(一般是在本地缓存或持久化存储中检索对象,可被认为是重建已经存在的对象)
  • 工厂是存粹的领域里的对象
  • 资源库会包含到基础设施的链接,例如数据库和缓存
    对于复杂新对象的创建过程
  1. 工厂创建好新的对象
  2. 对象被传递给资源库,资源库来保存对象

面向深层理解的重构

持续重构

重构通常是非常谨慎的 ,按照小幅且可控的步骤进行。

按领域模型重构

有时会对领域有新的理解,有些事物变得更加清晰,或者发现了两个元素间的关系,所有的这些会通过重构工作被包括到设计中。
使用迭代的重构过程,加上领域专家和开发人员一起密切关注对领域的学习,就能完成一个复杂且成熟的领域模式。
重构是小幅度进行的,其结果也必然是一系列小的改进

凸显关键概念

我们会为模型添加新的概念和抽象,然后对基于模型的设计做重构。每一次改进都会让设计更加清晰。这为取得突破创建了必要的前提。
为达到一次突破我们需要将隐含的概念显现出来,用来解释已经在领域中的其他概念。我们应该为它们创建类和关系

挖掘模型概念,将它显现出来,对设计做重构,让它更清晰、更具灵活性。
挖掘模型概念的方法:

  1. 解决领域内各方概念冲突的矛盾
  2. 使用领域文献
    将概念凸显的方式:
  3. 约束

约束是一个很简单的表达不变量的方式。
无论对象的数据如何变化,不变量都要得到保持简单.
实现方式是将不变量的逻辑放在一个约束中

  1. 过程

也叫处理过程,通常有两种方式,一种是封装到实体里,为实体添加一种行为。还有一种是使用服务(推荐),主要更能凸显处理的过程。

  1. 规约
    规约是用来测试一个对象是否满足特定条件的
    领域层包含了应用到实体和值对象上的业务规则。那些规则通常与它们要应用到的对象合成一体的。
    一些规则仅仅是一组答案为“是”或“否”的问题,这样的规则可以被表达为一系列在布尔值上执行的逻辑操作,最终的结果也是一个布尔值。

一个这样的例子是在一个客户对象上执行测试,看他是否有资格获得特定的贷款.这个规则可以被表达为一个方法,起名叫isEligible(),且可以附加在客户对象上。
但是这个规则不是简单的基于客户数据执行操作的简单方法。评估规则涉及很多,是否有债务逾期,信用等级是否达标等等。这个业务规则可以膨胀到很大很复杂。
在这种情况下,我们可能会将规则移动到应用层,因为这个规则看上去已经超越领域层了。这个时候就需要重构。

我们还是要把握原则:
规则应该被封装到其自身的一个对象中,这将成为客户的规约,并且被保留在领域层中。
我们可以将这若干个测试的方法封装到对象自身内,在检测的时候对这些测试的方法进行组合在一起,表达更复杂的规则。例如

Customer customer =
customerRepository.findCustomer(customerIdentiy);
…
Specification customerEligibleForRefund = new Specification(
new CustomerPaidHisDebtsInThePast(),
new CustomerHasNoOutstandingBalances());
if(customerEligibleForRefund.isSatisfiedBy(customer) {
refundService.issueRefundTo(customer);
}

保持模型的一致性

模型必须是一致的,保持不变的术语,并且没有矛盾。
模型内部的一致性被称为“统一”(unification)。
因为项目存在很多团队的合作,当模型的设计在局部独立进化的时,就很难维护一个大的统一的模型。
与其维护一个迟早要四分五裂的大模型,我们应该有意识地将大模型拆解成较小的模型。
只要模型间的关系能精确地定义,形成清晰地边界,小模型都能独立地进化。
下面的内容就是为这一目的的相关技术介绍及实现。

界定的上下文

每个模型都有一个上下文。模型的上下文是一些条件的集合,这些条件可以确保应用在模型里的术语都有一个明确的含义。
主要的思想是定义模型的范围,定出它的上下文的边界,然后尽最大可能保持模型的统一。
界定的上下文——通俗理解就是系统内部按照不同业务目的进行划分的「模块」
定义领域边界,以确保就可在统一的领域边界内用统一的语言进行交流,领域模型则存于该边界内。

如电商领域的商品,商品在不同阶段有不同术语,在销售阶段是商品,而在运输阶段则变成货物
**作用:

  1. 限界上下文就是用来细分领域,从而定义通用语言所在的边界。
  2. 封装通用语言和领域对象
  3. 提供上下文环境,保证在领域之内的一些术语、业务相关对象等
    这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,不应该在模型中实现。
    界定上下文与模块的关系
    界定的上下文并不是模块 。界定的上下文提供有模型在其中进化的逻辑框架 。模块是被用来组织模型的元素 ,因此界定的上下文包含了模块 。
    例子:

有一个电商网站,允许客户注册,用户登录后可以浏览购买商品然后下单。针对这个场景分析:
我们先要创建一个电子商务的模型,处理购买和下单
然后我们还需要发货给用户,我们需要建立一个仓库模型。这个模型接受电子商务模型的订单信息(这个信息包括商品条目、数量、用户信息,以值对象的方式接收),接收到了以后做相关备货、扣减库存、打包投递。
然后将货物与快递信息传递给物流系统(物流系统基本上属于公共服务的系统了,不属于这个电商系统的范围,如果物流系统也是属于公司业务的话,也要包含在内(比如京东,采用自己的物流))
实际上我们就得到了电商、仓库这两个独立的模型,只需要保证他们之间接口良好工作就行。

持续集成

概念
模型不是一开始就被完全定义。而是先被创建,然后基于对领域新的理解和来自开发过程的反馈持续进化。这意味着新的概念会进入模型,新的元素也会被添加到代码中。
所有的这些需求都会被集成进一个统一的模型,进而用代码来实现。

我们需要这样一个集成的过程,以确保所有新增的元素和模型原有部分能够和谐相处,在代码中也被正确地实现。
我们需要有一个过程用来合并代码。合并得越早越好。对于单个小团队,推荐做每日合并。
我们还需要有一个适当的构建过程(build process)。合并的代码需要自动地被构建,这样才能够被测试。

持续集成应用于界定的上下文内,集成后通过实践或测试,错误能够被检测出来,所以推荐集成的越频繁,越早越好。

上下文映射

预习概念

一个系统可能存在多个模型,每个模型都有自己的界定的上下文。
建议使用上下文作为团队组织的基础
尽管每个团队都工作于自己的模型,最好让每个人都能了解总体的图景
概念
上下文映射(Context Map)是描绘不同的界定上下文和它们之间关系的一份文档 。 形式可以是图表或其他形式的文档。
每个模型的功能都只是整个系统的一部分.
模型被组装在一起,整个的系统才能正常工作

模型上下文定义很重要

  1. 如果上下文定义的不清晰,不同模型很有可能彼此之间互相覆盖
  2. 如果上下文之间的关系,没有被描绘出来,在系统被集成的时候它们就有可能无法工作
    要素:定义清晰独立、标识出与其他上下文之间的关系

上下文映射的模式

共享内核(Shared Kernel)和客户-供应商(Customer-Supplier)————处理上下文之间的高级交互的模式。
隔离通道(Separatte Way)————在我们想让上下文高度独立和独立进化时要用到的模式。
还有两个模式(开放主机服务(Open Host Service) 和防崩溃层 ( Anticorruption Layer)。)————用来处理系统与一个遗留系统或一个外部系统。

共享内核(Shared Kernel)

创建不同领域共享的领域模型子集,能避免不同领域重复创建成本。当然还要注意此公共子集在修改时需要多方领域统一意见。
共享内核的目的是减少重复,但是仍保持两个独立的上下文

客户-供应商(Customer-Supplier)

场景:某公司有一个电商平台,现在公司想开发一个桌面端的报表系统,报表的数据来源是电商平台产生的交易数据。
问题分析:两个系统在同一个数据库工作,电商系统可能会因为业务对数据库的内容进行调整(比如表里新增字段,将A表里的字段挪动到B表),这对电商系统不是什么问题,但是对报表系统影响就大了,这种变动很可能对报表系统造成很大伤害。
这种场景就像电商系统生产数据,提供给报表系统使用。
电商系统————供应商
报表系统————客户
这两个系统对应的团队要定期碰面,报表团队阐述需求,报表团队做设计实现。
需要精确定义两个子系统之间的接口,在两个团队之间建立一个清晰的客户/供应商关系
如果客户不得不使用供应商团队的模型,而且这个模型做得很好,那么就需要顺从这个模型了。
客户团队遵从供应商团队的模型,完全顺从它。
与共享内核的区别
这和共享内核很相似,但有一个重要的不同之处。客户团队不能对内核做更改。他们只能将它作为自己模型的一部分,可以在所提供的现有代码上完成构建。

防崩溃层

在客户/供应商关系里,如果供应商团队的模型没有被很好地构思,导致其实现非常糟糕。虽然客户上下文仍然可以使用它,但是它应该通过使用一个“防崩溃层”来保护自己。
客户端应用不能在不理解被使用数据含义的情况下就访问数据库并执行写操作。我们看到外部模型的一些部分被反映在数据库里,然后影响了我们的模型。我们不能忽视与外部模型的交互,但是我们也应该小心地将我们的模型与外部模型隔离开来,尤其是在外部模型老旧、模型模糊不清的情境下。这时,我们应该在我们的客户端模型和外部模型之间建造一个防崩溃层。
如何实现防崩溃层

  1. 将这个层看作来自客户端模型的一个服务
  2. 使用服务是非常简单的,因为它抽象了其他系统并让我们以自己的术语来定位它.
  3. 服务会处理所需要的转换,所以我们的模型可以保持绝缘
  4. 防崩溃层有可能还需要一个适配器(Adapter),适配器可以使你将一个类的接口转换成客户端能够理解的另一个接口
  5. 考虑到实际的实现,可以将服务实现为一个外观模式。
    适配器将外部系统的行为包装起来 。
    我们还需要对象和数据转换(object and data conversion),可以使用一个转换器 ( translator) 来完成这个任务 。

隔离通道

如果我们得出的结论是集成的难度很大,不值得这样去做,那么就应该考虑隔离通道。
隔离通道适用情况
一个企业应用可由几个较小的应用组成,而且从建模的角度来看彼
此之间有很少或者没有公共之处。
在采用隔离通道模式之前,我们需要确信我们将不会回到一个集成的系统 。
独立开发的模型是很难做集成的 ,它们的相通之处很少 ,不值得这样做 。

开放主机服务

当我们试图集成两个子系统时,通常要在它们之间创建一个转换层.
这个层在客户端子系统和我们想要集成的外部子系统之间扮演了缓冲的角色
这个转换层设置在子系统层
当一个子系统要和其他很多子系统集成时,为每一个子系统定制一个转换器会产生大量重复相似的代码,维护会很困难。
解决思路:
将外部子系统看作服务提供者,在这个服务内封装一组服务,那么所有的其他子系统将会访问这些服务,我们也就不需要任何转换层。