领域驱动设计-笔记

本文整理自《DDD quickly》中文版,你可以在这里下载到免费的PDF版本。

简介

兴趣驱动:

我要在这个项目中使用苹果公司新推出的 Swift 编程语言,在服务器端要使用Hadoop,最好再尝试一下深度学习方面的技术”

人月神话:强调软件开发的概念完整性。DDD(领域驱动设计)是维护软件概念完整性的良药。

Evans:衡量一种技术好坏的最终标准,是这种技术是否有助于开发人员专注于研究领域问题。

需求的反向工程: 需求往往是从设计中反推出来的(糟糕的工作方式)

模型:对现实世界有选择性地精简。

从业务(领域)模型入手,而不是从技术层面入手。

MDD:模型驱动设计 MDA:模型驱动架构

DDD:传统OO泛型的探索和升级版

软件起源于其领域,并且与其领域密切相关,需要自动化的业务流程或者真实世界的问题,就是软件的领域。

让软件成为领域的一个映射,软件需要包含领域里最重要的核心概念和元素,并精确地实现它们之间的关系,即软件需要对领域进行建模。

用模型来交流。

领域抽象(领域理解):以空中飞行监控为例

演化:

与领域专家(空中交通控制人员)继续深入交流,反馈:
领域驱动设计-笔记_第1张图片

从领域的基本概念中,提炼出模型,模型作为软件开发人员与业务人员的交流工具-> 基于模型的语言,通用语言。

通用语言

出发地、目的地、路线、飞行计划、海拔高度、巡航高度、巡航速度、飞机类型。
模型继续演化:
领域驱动设计-笔记_第2张图片

沟通方式: 图、UML、文档

模型驱动设计

如何完成从模型到代码的转化。

分析模型:业务领域分析的结果,不需要考虑代码如何实现。–>模型与软件设计不匹配。

将领域模型与设计紧密关联起来,开发人员加入到建模过程。

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

领域驱动设计-笔记_第3张图片

分层架构(layered architecture)

将领域与其他软件部分分离:

领域驱动设计-笔记_第4张图片

  • 用户界面/展现层: 负责向用户展现信息以及解释用户命令。
  • 应用层:协调应用的活动,不包含业务逻辑,不保留业务对象的状态,但是保留应用任务的进度状态。
  • 领域层:保留业务对象的状态,核心业务逻辑。对象状态的持久化委托给基础设施层。
  • 基础设施层:作为其他层的支撑库,提供层间通信、持久化、界面支持等。

各层之间的交互例子:
例如,用户想要预定一个飞行路线,通过操作界面,请求应用层中的应用服务来做这件事情。应用层从基础层中取得相关的领域对象,然后调用他们的相关方法,例如检查安全限制等,完成之后修改领域对象的状态。应用服务奖对象持久化到基础设施。

实体(Entity)

拥有标识符,重要的不是属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的声明周期,这样的对象称为实体

不是实体:例如表示天气的类不是实体,人(Person)是一个实体。实体的核心标识是其标识符。例如银行卡账号,美国的社会保险号,身份证号等。

值对象(Value Object)

一个对象是否是一个实体:

  • 是否需要拥有一个标识符
  • 标识符有延续性吗

有时候我们只关系对象的属性,对它是哪一个对象不感兴趣。用来描述领域的特定方面,并且没有标识符的对象,叫做值对象

没有标识符, 值对象就可以被轻易地创建或者丢弃,可以被共享。 极力推荐将值对象实现为不可变的,他们由一个构造器创建,并且在整个生命周期内永远不会被修改。当想要得到这个对象的不同值的时候,简单地创建另一个对象就行。

如果值对象是可共享的, 那么它们应该是不可变的。 值对象应该保持
很小、 很简单。 当其他参与方需要一个值对象时, 可以简单地传递值, 或者创建一个副本。 制作一个值对象的副本是非常简单的, 通常不会有什么副作用。 如果没有标识符,你可以按你所需创建若干副本, 然后根据需要来销毁它们。

服务(Service)

  • 领域名词–>对象
  • 领域名词对应的动词–>对象的行为或者方法
  • 不属于任何对象的动词–>抽象成服务对象

例如账户之间的转账,转账这个动词到底应该属于转出还是转入,都不太合适,最佳实践是声明为一个服务。服务对象不再拥有内置的状态,而是为领域提供相应的功能。

服务:提供一个操作的接口。一个服务不是与执行服务的对象相关,而是与操作所要执行的所有对象相关。作为多个对象的连接点。
抽象出服务的一个好处是可以使得对象之间解耦。

服务的三个特征:
1. 服务执行的操作代表一个领域概念,但是这个概念无法自然地属于实体或者值对象。
2. 操作设计到领域中的其他对象。
3. 操作是无状态的。

在声明领域层的服务时,要注意与基础设施中的服务或者而应用层中的服务区分开来。服务一般建立在实体和值对象之上,一遍提供跟这些对象直接相关的服务。

例如,在报表系统中,允许用户创建特定类型的报表,报表基于数据库中的数据和报表模板产生,最后以HTML返回给WEB浏览器,同时允许用户登录。在该系统中,用户选择创建一个报表的时候,会传递一个reportId到应用层,这个id标识报表的类型。

领域层包含Report和Template两个模型,基础设施支持数据库访问和文件访问。当reportId被传递到领域层的时候,领域层负责解析这个id对应哪个Template,然后使用数据库的数据渲染模板,形成最终报表。

模板对象被保存在文件系统中,因此根据reportId获取对应的模板,这个功能不属于Report,也不属于Template,因此我们声明一个服务,这个服务根据reportId从文件系统中获取一个Template。

模块

模型数量达到一定程度后,需要对模型进行模块的划分,以便于管理、裂解他们之间的交互。将高度关联的模型组织成模块,有利于实现高内聚和低耦合的代码质量。

内聚
  • 通信性内聚(communicational cohesion):模块中的部件操作相同的数据时,得到通信性内聚。
  • 功能性内聚(functional cohesion):模块中的部件协同工作完成定义好的任务。

模块应该定义好接口,以便其他模块访问,直接将模块对象暴露给其他模块是不合适的。

聚合(cohesion)

聚合是一个用来定义对象所有权和便捷的领域模式。
模型之间的关联必不可少,来自模型的挑战往往不是让他们尽可能完整,而是尽可能简单和容易理解。

  • 删除模型中非基本关联的关系
  • 通过添加约束来减少多重关系
  • 尽可能将双向关联转化为单向关联(如汽车与发动机)

聚合是针对数据变化可以考虑成一个单元一组关联的对象。聚合使用边界将内部和外部的对象划分开来。每个聚合都有一个实体类型的根。并且它是外部可以访问的唯一的对象。外部对象只能持有对跟对象的引用。如果边界内还有其他实体,那些实体的标识符是本地化的,只在聚合内有意义。

将跟对象作为聚合对外的交互窗口,可以强化数据的一致性。根对象可以将内部对象临时传递给外部,但是操作完成后,外部对象不能再持有这个引用。一种实现方式是向外部对象传递对象的副本,在副本对象上发生什么不再重要。

一个聚合中的对象如果被保存到数据库,可以通过查询来获得的应该只有根对象,其他的对象只能通过从跟对象触发导航关联对象来获得。

聚合内的对象允许持有其他根对象的引用(根实体拥有全局的标识符)。

聚合的一个例子如下图:Customer是聚合的根,其他所有的对象都是内部的,如果需要地址,一个副本将被传递给外部。

工厂

实体和聚合通常会很大很复杂,以至于无法通过根实体的构造器来创建。事实上,通过构造器努力创建出一个复杂的聚合,并不是领域本身应该做的事情。在领域中,某些事物通常由另外一些事物来创建,使用根实体来构建复杂的聚合,就像是使用打印机来打印另一个打印机一样。工厂用来处理对象创建的问题。

如果一个对象的创建比较简单,通过传递一两个参数就可以完成,这时候由其客户端来调用构造器创建对象是合理的。但是如果一个对象的创建过程很复杂,涉及到很多内部复杂结构以及对象身上的规则限制,那么客户端将持有关于构建该对象的很多专用知识。这严重破坏领域对象和聚合的封装,使得客户端和聚合本身存在很紧密的耦合关系。如果客户属于应用层,那么领域层的一部分功能将被转移到应用层。

创建一个对象可以是自身的主要操作,但是复杂的组装操作不应该成为对象本身的职责。这个复杂的创建过程可以通过封装来实现,这就是Factory的功能,工厂封装对象创建所必须的知识,对于创建聚合特别有用。
保持对象创建过程的原子性,并且创建之后的对象所处的状态应该是有效的、一致的,不变量也必须创建完毕。

《设计模式》中介绍了2种模式的工厂:Factory Method和Abstract Method。都可以用于完成工厂的职能。

实体工厂和值对象工厂是不一样的。

有些情况下不需要使用工厂:

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

资源库

要使用对象,必须先获取对象,也即是持有对象的引用。如果是聚合根,那么它是一个实体,一般可以从数据库等持久化介质中找到。如果是值对象,可以通过从实体中顺着关联关系找到。但是,总是从数据库检索对象,会存在一些问题。

数据库是基础设施的一部分,直接从数据库检索对象,使得客户端必须知道数据库所需要的细节,使用SQL返回的一组记录,甚至会暴露更多细节。因此领域且一旦数据库发生变更,将会影响四处的代码。模型遭到了损害,它必须处理大量的基础设施细节,而不是领域概念。

因此,客户端需要一个使用的程序来获取已经存在的对象的引用。资源库就是用来处理这些问题,它保存对某些对象的引用,封装所有获取对象引用的逻辑,领域对象不再处理基础设施,重新获得领域模型应有的清晰和专注。资源库扮演一个全局可访问的对象存储地点,它可以包含一个策略,来决定从哪里或者怎样获取对象。这样,实现模型与基础设施的解耦。

领域驱动设计-笔记_第5张图片

对于需要全局访问的每种类型的对象, 创建一个对象来提供该类型所有对象都在内存中的假象。 通过一个众所周知的全局接口来设置访问途径。 提供方法来添加或者删除对象, 封装向数据存储中插入或者删除数据的实际操作。 提供基于某些条件选择对象的方法, 返回属性值符合条件的完全实例化的对象或对象集合, 从而封装实际的存储和查询技术。 仅仅为真正需要直接访问的聚合根提供资源库。 让客户程序保持对模型的专注,将所有的对象存储和访问细节都委托给资源库。

资源库也可以被看作是一个工厂, 因为它会创建对象。 然而它不是从无到有创建新的对象, 而是重建已有的对象。 我们不应该将资源库与工厂混合在一起。 工厂应该用来创建新的对象, 而资源库应该用来发现已经创建的对象。 当一个新对象被添加到资源库时,它应该是先由工厂创建好的, 然后它应该被传递给资源库, 由资源库来保存它。

重构

约束:

对象必须满足的一些条件,例如书架上存放的书不能超过其总容量。

public void add(Book book){
    if (content.size() + 1 <= capacity){
        content.add(book);
    }else {
        throw new IllegalOperationException("...");
    }
}

将约束提取出来:

public boolean isSpaceAvailable(){
    return content.size() + 1 <= capacity;
}

public void add(Book book){
    if (isSpaceAvailable()){
        content.add(book);
    }else {
        throw new IllegalOperationException("...");
    }
}

将约束独立出来,突显约束,是一个很好的方式。当约束变得复杂的时候,好处更加明显。

过程

处理过程在代码中被表述为procedure,建模时,我们需要为过程选择一个对象,最好的实现方式是使用服务。

规约

规约用于测试一个对象是否满足特定的条件。例如一个客户是否有资格得到特定的贷款: isEligiable()。如果这个规约很简单,可以实现为对象的一部分,但是如果要判断贷款的条件非常复杂,那么就不适合放在Customer对象中了。可以将规约移动到应用层中,但是更合理的方式是将规约封装为一个独立的对象,并且仍然保留在领域层。

新的对象将包含一组布尔方法, 这些方法用来测试一个客户对象是否有资格获得贷款。 每一个方法承担了一个小的测试功能, 通过组合所有的方法, 可以给出某个原始问题的答案。如果业务规则没有被包含在一个规约对象中, 对应的代码会散布到很多对象中, 难以保证规则的一致性。最后的代码片段可能像这个样子:

Specification customerEligibleForRefund = new Specification(...);

if ( customerEligibleForRefund .isSatisfiedBy(customer)){
    refundService.issueRefundTo(customer);
}

保持模型的一致性

界定的上下文

需要为每一个模型定义上下文。界定的上下文提供有模型在其中进化的逻辑框架。通过以下因素来明确设置边界:

  • 团队的组织架构
  • 应用惯例
  • 物理表现(如代码块,schema)

持续集成

上下文映射

Context Map用于描绘不同的界定上下文和他们之间的关系。
领域驱动设计-笔记_第6张图片

共享内核

客户-供应商

顺从者

防崩溃层

隔离通道

开放主机服务

提炼

你可能感兴趣的:(设计)