【笔记】DDD领域驱动设计

同名读书笔记,对于一些自觉重要的点进行记录。

扩展资源:github.com/evancyz/ddd-learning

最后的第四部分暂时没看


Part Two 模型驱动设计的构造块

Chapter 5 软件中所表示的模型

5.2 模式:Entity(Reference Object)

一个对象主要不是由他们的属性定义,而是有一个“标识”,但又不是如Java自带的标识(==)的形式,而是业务上的,如银行结算单和交易记录簿进行对账时就是具备相同标识的交易。Entity最基本的职责是确保连续性,以便其行为更清楚且可预测。与标识有关的属性尽量留在Entity内。当然实际上这种标识仍是由属性来实现。

5.3 模式:Value Object

eg. 画画时很容易区分出画(Entity)是谁画的,但有两只一样的笔,我们不会记住某个线条是其中哪只笔(VO)画的。

VO:用来描述领域的某个方面而本身没有概念标识的对象。(我们只关心它们是什么,而不关心它们是谁。)

但同一个对象在不同的场景下可能是Entity,也可能是VO,要具体分析。也不一定是Entity has VO,也可能是VO has Entity。如果说Entity之间的双向关联很难维护,那么两个VO之间的双向关联则完全没有意义。

VO应该是不可变的,不需要分配标识,VO所包含的属性应该形成一个概念整体。

5.4 模式:Service

有时候,对象不是一个事物。有些重要的领域操作无法放到Entity或VO中,因为这些操作本质上是一些活动或动作,而非事物。

Service不具有封装的状态,它强调的是与其他对象的关系。Servie的特征:

  1. 与领域概念相关的操作不是Entity或VO的一个自然组成部分。
  2. 接口是根据领域模型的其他元素定义的。
  3. 操作是无状态的。(任何客户都可以使用某个Service的任何实例)

定义接口时要使用模型预言,并确保操作名称是UBIQUITOUS LANGUAGE中的术语。

可根据是否包含专业业务规则将Service分为“应用Service”和“领域Service”。

5.5 模式:Module 模块(或Package)

Module之间应该是低耦合的,Module内部则是高内聚的。

打包时注意:对象的一个基本概念时将数据和操作这些数据的逻辑封装到一起。

除非真正有必要将代码分布到不同的服务器上,否则就把实现单一概念对象的所有代码放在同一个模块中。

5.6 建模范式

对象范式

混合范式

Chapter 6 领域对象的生命周期

6.1 模式:Aggregate

eg. 当删除一个用户时,我们如何处理地址呢?可能还有其他人住在同一个地址,删除的话,其他用户变为引用空对象。保留的话,垃圾地址会积累其他。——在具有复杂关联的模型中,要想保证对象更改的一致性是很困难的。

首先需要一组抽象来封装模型中的引用。Aggregate就是一组相关对象的集合,我们把它作为数据修改的单元。每个Aggregate都有一个根(root)和一个边界(boundary)。外部对象只可以引用根,而边界内部的对象之间可以互相引用。

  • Aggregate内部的对象可以保持对其他Aggregate根的引用。
  • 删除操作必须一次删除Aggregate边界之内的所有对象。

参考P85示例。

6.2 模式:Factory

对象的功能主要体现在其复杂的内部配置以及关联方面。我们应该一直对对象进行提炼,直到所有与其意义或在交互中的角色无关的内容被完全剔除为止。

eg. 汽车只有在被生产时才需要装配工,在我们驾驶时并不需要,由于汽车的装配和驾驶永远不会同时发生,将这两种功能合并到同一个机制中毫无价值。——将创建复杂对象的实例和Aggregate的职责转移给单独的对象,即Factory。

接口的设计。当设计Factory的方法签名时,需要注意:

  • 每个操作都必须是原子的。必须确定创建失败时的操作。
  • Factory将与其参数发生耦合。若只是将参数插入,依赖度是适中的,若是选出一部分在构造对象时使用,耦合将更为紧密。

由于Entity需要“标识”,Entity Factory和VO Factory有所不同。

重建已存储的对象时也需要注意。

6.3 模式:Repository

无论对象执行什么操作,都需要保持对它的引用,那么如何获得这个引用呢?采用关系型数据库使得人民自然而然地使用基于对象的属性来获取引用。——执行查询来找到对象,然后重建它。

从概念上讲,对象检索发生在Entity生命周期的中间,我们把用已存储的数据创建实例的过程称为重建

大多数对象都不应该通过全局搜索来访问。——随意的数据库查询会破坏领域对象的封装和Aggregate。技术基础设施的和数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的设计。

Repository解除了客户的巨大负担,使客户只需要与一个简单的、易于理解的接口进行对话。它们使得应用程序和领域设计与持久化技术解耦。

Factory与Repository:

  • Factory负责处理对象生命周期的开始,而Repository帮助管理生命周期的中间和结束。
  • Factory负责制造新对象,而Repository负责查找已有对象。

引申:《DAO还是Repository,傻傻的分不清?》

关于 Dao 和 Repository 在技术上,假设如果不用区分的那么细。但在语义上就很重要了,因为当我看到 repository 的时候,想到的设计聚合,而 dao 会第一个想到是一个贫血Entity。这也是 模式 的一个重要作用

进一步可以参考:《阿里技术专家详解DDD系列 第三讲 - Repository模式》

传统Data Mapper(DAO)属于“固件”,和底层实现(DB、Cache、文件系统等)强绑定,如果直接使用会导致代码“固化”。所以为了在Repository的设计上体现出“软件”的特性,主要需要注意以下三点:

  1. 接口名称不应该使用底层实现的语法:
  2. 出参入参不应该使用底层数据格式:需要记得的是Repository操作的是Entity对象(实际上应该是Aggregate Root),而不应该直接操作底层的DO。更近一步,Repository接口实际上应该存在于Domain层,根本看不到DO的实现。这个也是为了避免底层实现逻辑渗透到业务代码中的强保障。
  3. 应该避免所谓的“通用”Repository模式

Chapter 7 使用语言:一个扩展的示例

系统设计过程:

  1. 模型设计(UML类图)
  2. 隔离领域:引入应用层
  3. 区分Entity和VO:依次考虑每个对象,看看这个对象是必须被跟踪的实体还是仅表示一个基本值。(或思考两个完全相同的对象是否需要区分开?)
  4. 设计领域类间的关联关系(引用关系)。若存在循环引用,需要考虑数据库设计的问题。
  5. 划分Aggregate边界。
  6. 选择Repository
    1. 上一步有多少Entity是Aggregate的根,则选择存储库的时候只需要考虑这几个实体,其他对象都不能有Repository。
    2. 回看需求,重新确定这些实体确实需要Repository。
  7. 场景走查
  8. 对象的创建:思考是否需要为其中重要的类创建Factory方法。
  9. 重构
    • 并发问题:考虑某项修改操作的使用频率是否会带来对应Aggregate的并发问题。是否可以使用其他技术来替代该操作?eg. 用查询来替代维护一个集合。(又涉及到Repository的问题)。
  10. 划分Module:我们应该寻找紧密关联的概念,并弄清楚我们打算向项目中的其他人员传递什么信息。
  11. 引入新特性(功能)
    1. 链接两个系统(与已有的另一个系统对接)
    2. 进一步完善模型:划分业务。可能需要添加中间类来与其他系统的接口对接。
    3. 性能优化。eg. 上述的中间类可以添加缓存功能来提升性能。
  12. 小结:对于Enterprise Segment的获取:正确的做法是让那些知道划分规则的对象来承担获取这个值的职责,而不是把这个职责施加给包含具体数据(那些规则就作用于这些数据上)的对象。

Part Three 通过重构来加深理解

深层模型:深层模型能够穿透表象,清楚地表达出领域专家们的主要关注点以及最相关的知识。

Chapter 8 通过重构来加深理解

实际上是特定领域模型的理解加深或实际需求增多/复杂化来驱动的重构。而重构的过程本身又会进一步加深对模型的理解。

Chapter 9 将隐式概念转变为显式概念

深层建模的第一步就是要设法在模型中表达出领域的基本概念。开发人员在消化知识或重构的过程中,识别出某个隐含的概念,并将其显式地表达出来。

概念挖掘:与领域专家不断交流。

模式:Specification(举得例子实际就是 调用链模式)

Chapter 10 柔性设计

  • 10.1 模式:Intention-revealing interfaces(释义接口)
    • 如果开发人员为了使用一个组件而必须要去研究它的实现,那么就失去了封装的价值。
    • 在命名类和操作时要描述他们的效果和目的,而不要表露他们是通过何种方式达到目的的。
    • 整个子领域可以被划分到独立的模块中,并用一个表达了其用途的接口把他们封装起来。
  • 10.2 模式:Side-Effect-Free Function(无副作用方法)
    • 我们可以宽泛地把操作分为两个大的类别:命令和查询(P.S. 相当于读和写)
    • 多个规则的相互作用或计算结果的组合所产生的结果是很难预测的。开发人员在调用一个操作时,为了预测操作的结果,必须理解它的实现以及它所调用的其他方法的实现。如果开发人员不得不“揭开接口的面纱”,那么接口的抽象作用就收到了限制。
    • 尽可能把程序的逻辑放到函数中,因为函数是只返回结果而不产生明显副作用的操作。严格地把命令(引起明显的状态改变的方法)隔离到不返回领域信息的、非常简单的操作中。当发现了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂逻辑移到Value Object中,这样可以进一步控制副作用。

P. S. 实际上例子是对于两个类A1和A2,将原设计中类A中被改变地部分属性抽出来类B(VO类),即A1、A2分别带有B1、B2,而A1、A2操作时不改变自己的B1、B2,而是生成B3返回。

  • 10.3 模式:Assertion(断言)
    • 如果操作的副作用仅仅是由他们的实现隐式定义的,那么在一个具有大量相互调用关系的系统中,起因和结果会变得一团糟。理解程序的唯一方式就是沿着分支路径来跟踪程序的执行。封装完全失去了价值。跟踪具体的执行也使抽象失去了意义。
    • 把操作的后置条件和类及Aggregate的固定规则表述清楚,如果在你的编程语言中不能直接编写Assertion,那么就把他们编写成自动的单元测试。还可以把他们写到文档或图中(如果符合项目开发风格的话)

P.S. 书中的例子是编写了单元测试。回想一下深度学习Python框架倒是常用assert检查矩阵维度。

  • 10.4 模式:Conceptual Contour(概念轮廓)
    • 如果把模型或设计的所有元素都放在一个整体的大结构中,那么他们的功能就会发生重复。
    • 而另一方面,把类和方法分解开可能也是毫无意义的,这会使客户更复杂,迫使客户对象去理解各个细微部分是如何组合在一起的。
    • 通过反复重构最终会实现柔性设计。
    • 寻找在概念上有意义的功能单元,这样可以使得设计既灵活又易懂。例如,如果领域中对两个对象的“相加”是一个连贯的整体操作,那么就把它作为整体来实现。不要把add()拆分成两个独立步骤。
    • 把设计元素(操作、接口、类和Aggregate)分解为内聚的单元,在这个过程中,你对领域中一切重要划分的直观认识也要考虑在内。在连续的重构过程中观察发生变化和保证稳定的规律性,并寻找能够解释这些变化模式的底层Conceptual Contour。使模型与领域中那些一致的方面(正是这些方面使得领域称为一个有用的知识体系)相匹配。
  • 10.5 模式:Standalone Class(独立的类)
    • 互相依赖使模型和设计变得难以理解、测试和维护。
    • Module和Aggregate的目的都是为了限制互相依赖的关系网。
    • 低耦合是对象设计的一个基本要素。尽一切可能保持低耦合。把其他所有无关概念提取到对象之外。这样类就变得完全独立了,这就使得我们可以单独研究和理解它。
  • 10.6 模式:Closure Of Operation(闭合操作)
    • 在适当的情况下,在定义操作时让它的返回类型和与其参数的类型相同。如果实现者(implementer)的状态在计算中会被用到,那么实现者实际上就是操作的一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。闭合操作提供了一个高层接口,同时又不会引入对其他概念的任何依赖。

eg. 从Collection中选择元素子集,需要使用Iterator,但是是否有必要使用Iterator这个额外概念以及其带来的额外复杂性呢?

  • 10.7 声明式设计
    • 通常指一种编程方式——把程序或程序的一部分写成一种可执行的规格(specification)。使用声明式设计时,软件实际上是由一些非常精确的属性描述来控制的。如,可以通过反射机制来实现,或在编译时通过代码生成来实现(根据生命来自动生成传统代码)。这种方法使其他开发人员能够根据字面意义来使用声明。它是一种绝对的保证。
    • 问题:把软件束缚在一个由自动部分构成的框架之;代码生成技术破坏了迭代循环,把生成的代码合并到手写的代码中。
    • 声明式设计发挥的最大价值时用一个范围非常窄的框架来自动处理设计中某个特别单调且易出错的方面,如持久化和对象关系映射。
  • 10.8 声明式设计风格
    • 一旦你的设计中有了Intention-revealing interfaces(释义接口)、Side-Effect-Free Function(无副作用方法)和Assertion(断言),就具备了使用声明式设计的条件。

在之前的Chapter 9 的 Specification中实际使用了调用链,但是调用链只能作为串行的条件过滤使用,如其中所说的为化学品安排容器,但是也应该尽量安排成本低的容器,所以要扩展and、or、not的接口,现在将这几种操作也分别封装成一个类,继承同一个抽象类。

  • 10.9 切入问题的角度

eg. 经过改造,贷款相关业务代码改造为了

  • 复杂的逻辑通过Side-Effect-Free Function被封装到了专门的Value Object中
  • 修改状态的操作很简单,而且是用Assertion来描述的。
  • 模型概念除了耦合,操作只涉及最少的其他类型。

Chapter 11 应用分析模式

分析模式的最大作用是借鉴其他项目的经验,把那些项目中有关设计方向和实现结果的广泛讨论与当前模型的理解结合起来。

关联了Martin Fowler的《分析模式》一书

Chapter 12 将设计模式应用于模型

  • 12.1 模式:Strategy(Policy)策略模式
  • 12.2 模式:Composite 组合模式
    • 定义一个把Composite的所有成员都包含在内的抽象类型。在容器上实现那些查询信息的方法时,这些方法返回由容器内容所汇总的信息。而“叶”节点则给予它们自己的值来实现这些方法。客户只需使用抽象类型,而无需区分“叶”和容器。

组合设计模式

Chapter 13 通过重构得到更深层的理解

重构的时机:

  • 设计没有表达出团队对领域的最新理解
  • 重要的概念被隐藏在设计中了(而且你已经发现了把他们呈现出来的方法)
  • 发现了一个能令某个重要的设计部分变得更灵活的机会
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值