领域驱动设计

关注精简的业务模型及实现的匹配

如果你了解”模型”的定义是对现实的有选择性的精简,然后用这样的观点去读 DDD 这本书,你就会发现,DDD 其实没有什么太多的新鲜玩意,它更多地是可以看作是面向对象思潮的回归和升华。在一个”万事万物皆对象”的世界里,哪些对象是对我们的系统有用的?哪些是对我们拟建系统没有用处的?我们应该如何保证我们选取的模型对象恰好够用?

前面的选择性问题只是解决了一个初步框选的问题,对象并不是独立存在的,它们之间有着千丝万缕的联系。这种扯不断理还乱的联系构成了系统的复杂性。一个具体的体现就是,我们修改了一处变更,结果引发了一系列的连锁反应。虽然对象的封装机制可以帮我们解决一部分问题,但那只是有限的一部分。我们应该如何在一个更高点的层次上,通过保留对象之间有用的关系去除无用的关系,并且限定变更影响的范围以来降低系统的复杂度呢?即在抽象的角度做业务。

在 DDD 以及传统 OO 的观点中,业务而不是技术是一个开发团队首先要关注的内容,众多的框架和平台产品也在宣称把开发人员解放出来,让他们有更多的精力去关注业务。但是,当我们真正去看待时,会发现,开发人员大多还是沉溺于技术中,对业务的理解和深入付出的太少太少。其实要解决这个问题,就要先看清楚我们提炼出来的模型,在整个架构和整个开发过程中所处的位置和地位。我们经常听到两个词,一个是 MDD(模型驱动设计),一个是MDA(模型驱动架构)。如果 DDD 特别关注的是”M”(以及其实现),那么,这个 M 应该如何与架构和开发过程相融合呢?我经常会看到我们辛苦提取出来的领域模型被肢解后,分散到系统的若干角落。这真是一件可怕的事情,因为一旦形成了”人脑拼图”,就很难再有一个人将它们一一复原,除非这个人是个天才。

很多面向对象的教材,都会告诉你若干的技巧,让你去机械化地处理模型和对象实现,但是这些教材通常会忽略一个大的上下文环境,就是应该由哪些具有什么样素质和技能的人来处理模型和对象实现,或者说白了,就是应该用什么样的团队模型来匹配业务模型。不同的模型,需要不同的团队模型的支撑,不同的团队模型也会让一个模型实现更优秀或者更糟糕。

相信很多人读过 ATM 机的例子,你发现自己彻底明白了用例应该怎么编写、模型怎么提取和实现,但是当你信心十足地去开始你自己的项目时,你又会发现你的思路片段化了,所有你明白了的技能在你的新项目中好像用不上。算了,还是老的工作思路和工作方式比较顺手,于是一切都照旧会到了老的套路上。那么,面向对象技术或者说我们提炼出来的模型应该如何在大型项目/团队中使用呢?我们是应该要求一个项目使用统一的模型,还是应该把它们分成不同的模型?我们应该如何抉择?

领域驱动设计

软件开发通常被应用到真实世界中已经存在的自动化流程,或者给真实的业务问题提供解决方案。即软件脱胎于领域,并与领域密切相关。

提出问题

以汽车制造为例。参与汽车制造的工人会专门负责汽车的某个部件,但这样做的后果是工人们通常对整体的汽车制造流程缺乏了解。他们可能将汽车视为一大堆需要固定在一起的零件的集合体,但一辆汽车的意义远不只于此。一辆好车起源于一个好的创意,开始于认真制定的规格说明,然后再交付给设计。经历若干道设计工序,(历经岁月),用上几个月甚至几年的时间去设计、修改、精化直至完美,直至它反映出最初的愿景。设计的过程也不全然是在纸上进行的。许多的设计工作包括制模、在极端条件下对它们进行测试,以验证它们是否能工作等。设计会根据测试的结果做出修改。汽车最终被交付到生产线上,在那里,所有的部件已经就绪,然后被组装到一起。

因此,为了创建一个好软件,你必须知道这个软件究竟是什么。在你充分了解金融业务是什么之前,你是做不出一个好的银行业软件系统的,你必须理解银行业的领域

那么对于软件架构师,他只是在使用银行来保护他的财产安全,以保证他的急时所需;软件分析师吗?也不是,他只精通于如何运用所有能够获得的必要因素去分析一个给定的主题;软件开发人员?别难为他了。

真正明白领域的人是业务人员。银行业务系统被银行的内部人员所熟知,我们称其为专家。他们知道所有的细节,所有的困难、所有可能出现的问题,以及所有的规章等。这些就是我们永远的起始点:领域。

背景

目标

在启动一个软件项目时,我们应该关注软件涉及的领域。软件的最终目的是增进一个特定的领域。为了达到这个目的,软件需要跟要它服务的领域和谐相处,否则,它会给领域引入麻烦,产生障碍、灾难甚至导致混乱等。

做什么

让软件成为领域的反射(映射)。软件需要具现领域里重要的核心概念和元素,并精确实现它们之间的关系。软件需要对领域进行建模。

需要建立领域的抽象——在脑海当中建立一个蓝图。这个抽象是一个关于领域的模型,并不是一个图,是那副图要极力传递的思想。

模型是我们对目标领域的内部展现方式,会贯穿设计和开发的全过程。

怎么做

一个领域当中包含着海量的信息。我们需要组织信息,将其系统化,分割成一个小一点的信息块,将这些信息块分类放到逻辑模块当中,每次只处理其中的一个逻辑模块。

我们需要忽略领域中的很多部分,因为信息太多,不能放到一个模型当中,并且也存在一些信息并不需要我们去考虑。这同样是一个挑战。

模型

模型是软件设计中的最基础的部分。我们需要它,是因为能够用它来处理复杂问题。我们对领域的所有的思考过程被汇总到这个模型中。

我们需要用模型与其他人进行交流。常见的一种方式是将模型图形化:图、用例、画和图片等。另一种方式是写,我们会写下我们对领域的愿景。还有一种方式是使用语言,我们能够也应该针对要交流的领域内的特定问题建立一种语言。

软件设计

软件设计类似于构建房子的架构,那是跟一个总图相关的。代码设计是非常细节性的工作,类似于在一面墙上定位一幅油画。

  • 瀑布设计方法
    • 业务专家提出一堆需求同业务分析人员进行交流,分析人员基于那些需求来创建模型并作为结果传递给开发人员
    • 开发人员根据他们收到的内容开始编码
    • 缺陷:业务专家得不到分析人员的反馈信息,分析人员也得不到开发人员的反馈信息。
  • 敏捷方法学
    • 产生背景:
      • 预先很难确定所有的需求,特别是需求经常变化的情况。要想预先创建一个覆盖领域所有方面的完整模型确实很困难
      • “分析瘫痪”,团队成员会因为害怕做出任何设计决定而无所事事。
    • 使用大量灵活的实现,通过由业务涉众持续参与的迭代开发和许多重构,开发团队更多地学习到了客户的领域知识,从而能够产出满足客户需要的软件。
    • 缺陷:
      • 他们提倡简单,但每个人都对“简单”的意义有着自己的观点。
      • 同时,缺乏了真实可见的设计原则,由开发人员执行地持续重构会导致代码更难理解或者更难改变。
      • 虽然瀑布方法可能会导致过度工程,但对过度工程的担心可能会带来另一种担心:害怕做出深度、彻底的设计

构建领域知识

通用语言

模型驱动设计

软件开发过程的重点,以业务领域为中心。让模型植根于领域,并精确反映出领域中的基础概念是建立模型的一个最重要的基础。

  • 建立模型
    • 在业务层通过接口确立模型,进行模型间的交互?
  • 将模型实现代码。
    • 在模型确立后,用代码进行实现模型,进行不断的迭代?

模型驱动设计的基本构成要素

模式与模式间的关系

1562555825617

分层架构

img

当创建一个软件应用时,应用的很大一部分是不能直接与领域相关的,但是它们或时基础设施的一部分,或是为软件服务的。如数据库访问、文件、用户界面等相关代码

为了开发一个每个层内聚的设计,让层仅仅依赖于下面的层。一个统一的架构包含4个概念层

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

实体

定义

有一类对象看上去好像拥有标识符,它的标识符在历经软件的各种状态后仍能保持一致。对这些对象来讲这已经不再是它们关心的属性,这意味着能够跨越系统的生命周期甚至能超越软件系统的一系列的延续性和标识符。我们把这样的对象称为实体。

标识符不是说一个对对象的引用。相反一个对象经常会被移出或移入内存,被序列化后在网络上传输,在另一端重新建立,或者都被消除。但是我们依然可以知道这个对象到底是什么。例如传过来一个“账户”的对象,我们还是可以确定这个账户到底是什么。

举例

考虑一个银行系统,每一个账户都拥有它自己的数字码,一个账户可以用它的数字码来精确标识。这个数字码会在系统的生命周期中会保持不变,并保持延续性。

账户码可以作为一个对象存在于内存中,也可以被在内存中销毁,发送到数据库中。当这个账户被关闭时,它还可以被归档,只要还有人对它感兴趣,它就依然在某处存在。不论它的表现形式如何,数字码会保持一致。

实现

创建实体,即意味着创建标识符。通常标识符或是对象的一个属性(或属性的组合),一个专门为保存和表现标识符而创建的属性,也或是一种行为。

对两个拥有不同标识符的对象来说,能用系统轻易地把它们区分开来。或者两个使用了相同标识符的对象能被系统看成是相同的,这些都是非常重要的

值对象

如何界定实体与值对象

实体是领域模型中必需的对象。如何去确认一个实体?我们是否应该将所有的对象视为实体?每一个对象都应该有一个标识符吗?

若是将所有对象看成是一个实体,那么由于实体是可以被跟踪的。创建与跟踪标识符需要很大的成本,需要保证每一个实体具有唯一的标识,并且根据标识符可以唯一去确定实体。同时会带来性能问题,如果Customer 是一个实体对象,那么这个对象的一个实例标识一个特殊的银行客户,不能被对应其他客户的账户操作所复用,造成的结果是必须为每一个客户建立一个这样的实例。这会导致系统在处理成千上万的实例时性能严重下降。

值对象

我们对某个对象是什么不感兴趣,只关心它拥有的属性。用来描述领域的特殊方面、且没有标识符的一个对象,叫做值对象。

实体对象

选择那些符合实体定义的对象作为实体

值对象的使用

没有标识符,值对象可以很轻易地被创建和丢弃。

极力推荐值对象是不变的,由一个构造器创建,并且在生命周期内永远不会被修改。这样值对象就可以被共享了,不变的对象可以在重要性能语境下共享,也能表明一致性。

如果值对象是可共享的,那么它们应该是不可变的。
值对象应该保持尽量的简单。当其他当事人需要一个值对象时,可以简单地传递值,或者创建一个副本。

服务

当分析领域并试图定义构成模型的主要对象时,有些方面的领域很难映射为对象。

对象通常要考虑拥有属性,对象管理它的内部状态并暴露行为。对应的动词可以映射为对象的行为。但是领域中的一些动作,看上去不属于任何的对象,代表着领域中的一个重要行为,通常这种动作会跨越若干个对象。

当识别出这样的动作,最佳实践是将它声明成一个服务。这样的对象不再拥有内置的状态了,它的作用是为了简化所提供的领域功能。服务所能提供的协调作用是非常重要的,一个服务可以将服务于实体和值对象的相关功能进行分组。最好显式声明服务,因为它创建了领域中的一个清晰的特性,它封装了一个概念。把这样的功能放入实体或者值对象都会导致混乱,因为那些对象的立场将变得不清楚。

服务是多个对象的一个链接点

服务的特征

  • 服务执行的操作涉及一个领域概念,这个领域概念通常不属于一个实体或者值对象。
  • 被执行的操作涉及到领域中的其他的对象。
  • 操作是无状态的

服务的设计

在使用服务时,需要保持领域层的隔离,不应当弄混属于领域层的服务和属于基础设施层的服务。

决定服务是否属于领域层

如果所执行的操作概念上属于领域层,那么服务就应该放到这个层。如果操作和领域对象相关,而且确实也跟领域有关,能够满足领域的需要,那么它就应该属于领域层。

模块

  • 对于大型的复杂项目,模型较为复杂,为降低复杂性,将模型组织进模块当中
  • 代码应该具有高层次的内聚与低层次的耦合度,将内聚组织到模块当中

聚合

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

提出问题

一个领域会包含众多的领域对象,我们总会看到许多对象与其他对象发生关联,形成一个复杂的关系网。领域对象间的实际关联在代码中结束,有时甚至却在数据库中。

直到模型中嵌入了对领域的深层理解,否则就要时常对模型中的关系进行消减和简化。

关联关系

对于1对1的关系。会表现为两个对象间的引用,并且在数据库表中隐含一个关联关系。

对于1对多的关联关系。可以被简单转化为一个对象与一组其他对象间的关联,虽然不总是行得通

对于多对多的关联。大部分是双向的,使得对这样的对象的生命周期管理变得困难。关联的数字应该被尽可能地消减。

  • 首先删除模型中非本质的关联关系
  • 增加约束,以消减多重性。如果很多对象满足一种关系,那么在这个关系上加入正确的约束后,很有可能只有一个对象会继续满足这种关系
  • 转换为非双向的关联。

数据一致性问题

当系统完全删除一个客户的信息时,必须保证所有的引用都被删除了。如果许多对象保有这样的引用,则很难保证它们全被删除了。

同时当客户的某些数据发生了变化,系统必须确保在系统中执行了适当的更新。通常在数据库层面进行处理,手段是利用事务。若没有仔细设计模型,则会导致很大程度的数据库争夺导致性能佷差。

不变量

不变量是数据发生变化时必须维护的那些规则。这在许多对象与数据发生变化的对象保持引用时更难实现。

聚合定义

聚合是针对数据变化可以考虑成一个单元的一组相关的对象。聚合使用边界将内部和外部的对象划分开来。

每个聚合有一个根。这个根是一个实体,并且它是外部可以访问的唯一的对象。根可以保持对任意聚合对象的引用,并且其他的对象可以持有任意其他的对象,但一个外部对象只能持有根对象的引用。如果边界内有其他的实体,那些实体的标识符是本地化的,只在聚合内有意义。

聚合原理

因为其他对象只能持有根对象的引用,意味着它们不能直接变更聚合内的其他的对象,只能对根进行变更,或让根来执行某些活动。

当删除根后,聚合的所有其他对象也都删除了,因为不会有其他对象持有他们当中的任何一个。

根对象可能将内部的临时引用传递给外部对象,作为限制,当操作完成后,外部对象不能再持有这个引用。(传递一个值对象的拷贝)

如果聚合对象被保存到数据库当中,只有根可以通过查询获得。

1562565186285

工厂

实体和聚合通常会很大很复杂,根实体的构造函数内的创建逻辑也会很复杂。通过构造器构建一个复杂的聚合与领域本身也会很冲突。

工厂用来封装对象创建所必需的知识,他们对创建聚合十分有效。

注意

工厂破坏了一个对象的封装原则,当对象中发生了某种变化,会对构造规则或者某些不变量造成影响,需要确认工厂也被更新以支持新的条件。

使用构造函数的情况

  • 构造过程不复杂
  • 对象的创建不涉及到其他对象的创建,所有的属性都需要传递给构造函数
  • 客户程序对实现很感兴趣,可能希望选择使用策略模式
  • 类是特定的类型,不涉及继承,所以不用在一系列的具体实现中进行选择

资源库

提出问题

创建对象的整体是为了能够使用他们。而想要获得这样的引用,客户程序必须创建一个对象或者通过导航已经有的关联关系从另一个对象中获得它。

问题是,客户程序首先要获得对根的引用,对大型应用而言,我们必须保证客户始终对需要的对象保持引用。者会强制要求对象持有一系列他们可能其实并不需要保持的一系列的引用。增加了耦合性,创建了一系列不需要的关联。

若是客户程序从数据库当中访问,获得它所需要的对象,看似简单,但是会对设计产生负面的影响

  • 数据库是基础设施一部分,客户程序必须要知道访问数据库所需的细节。
  • 访问数据库可能会暴露其内部更细节的信息。
  • 进行数据库访问的代码在整个模型中四散,在领域模型当中处理许多基础设施的细节而不是领域概念。

资源库

目的是封装所有获取对象引用所需的逻辑,领域对象不需要处理基础设施,以得到领域中对其他对象的所需的引用,只需要从资源库中获取。

资源库作为一个全局的可访问对象的存储点而存在。资源库会保存对某些对象的引用,当一个对象被创建出来时,可以被保存到资源库中,然后以后使用时可以从资源库中检索。

1562567548062

面向深层理解的重构

保持模型一致性

参考