Skip to content

janwee-sha/ddd-in-action

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 

Repository files navigation

DDD入门

为什么需要DDD

  • 使领域专家和开发者在一起工作,这样开发出来的软件能够准确地传达业务规则。
  • 可以帮助业务人员自我提高。
  • 集中知识,确保软件只是并不只是掌握在少数人手中。
  • 使用通用语言,使得每个人都能听懂他人所说。
  • 设计就是代码,代码就是设计。
  • DDD同时提供了战略设计和战术设计。

贫血对象

缺乏内在行为的领域对象。

DDD的两大支柱

通用语言

团队共享的语言。

Bounded Context(限界上下文)

应用程序内的概念性边界。

领域、子域和限界上下文

领域

即是一个组织所做的事情以及其中所包含的一切。

子域

领域的功能拆分,几乎所有软件的领域都包含多个子域。

限界上下文是显式的,充满语义的

限界上下文是一个显式边界,领域模型便存在于边界之内。在边界内,通用语言中的所有术语和词组都有特定的含义,而模型需要准确地反应通用语言。

限界上下文不仅仅只包含模型

限界上下文并不只局限于容纳模型,它通常标定了一个系统、一个应用程序或者一种业务服务。

架构

分层架构(Layered Architecture)

每层只能与位于其下方的层发生耦合。改进分层架构--依赖倒置原则,高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该应该依赖于抽象。

分层架构将领域分离出来,在业务逻辑与技术实现之间划定一条清晰的边界。

六边形架构

也被称为端口与适配器架构、onion架构。对于每种外界类型,都有一个适配器与之对应,外界通过应用层API与内部进行交互。

面向服务架构(Service-Oriented Architecture)

该架构的原则是服务契约,即通过契约文档,服务阐述自身的目的和功能。

特性

  • 松耦合:服务将依赖关系最小化。

  • 服务抽象:服务只发布契约,面向客户隐藏内部逻辑。

  • 服务重用性:一种服务能被其他服务重用。

  • 服务自治性:服务自行控制环境和资源以保持独立性。

  • 服务无状态性:服务负责消费方的状态管理。

  • 服务可发现性:客户可以通过服务元数据来查找服务和理解服务。

  • 服务可组合性:一种服务可以由其他服务组合而成。

实体

表示某种具有连续性和标识的事物。引用对象通常需要与另一个具有不同属性的对象相联系,且需要与具有相同属性的另一个对象区分开。

唯一的身份标识和可变性将实体和值对象区分开来。

身份标识生成策略

  • 用户提供
  • 应用程序生成
  • 持久化机制生成
  • 另一个限界上下文提供

角色与职责

领域对象扮演多种角色

在面向对象编程中,通常由接口来定义实现类的角色。一个类对于每一个它所实现的角色,都存在一种角色。如果一个类没有显式的角色,即表明该类没有实现任何显式接口。

创建实体

实体可能维护了一个或多个不变条件(Invariant),即在整个实体生命周期中都必须保持事务一致性的一种状态。

值对象

用于描述某种状态的属性。跟踪实体的标识非常有必要,但为其他对象也加上标识会影响系统性能并增加分析工作。 当我们只关心一个模型元素的属性时,应该把它归类为值对象,值对象应该是不可变的。

值对象的特征

  • 它度量或者描述了领域中的一件东西。
  • 它可以作为不变量。
  • 它将不同的相关的属性组合成一个概念整体。
  • 当度量和描述改变时,可以用另一个值对象予以替换。
  • 它可以和其他值对象进行相等性比较。
  • 它不会对协作对象造成副作用。

不变性

一个值对象在创建之后便不能改变了。

用值对象表示标准类型

枚举、状态模式(创建一个包含了需要支持的所有行为的抽象基类,然后为每个实际状态创建一个实体类来覆盖抽象基类的行为)、聚合都可以用来表示标准类型。 使用聚合表示标准类型时,每一个聚合实例代表一种类型。但此时作为消费方的限界上下文并不会维护标准类型。北大范围使用的标准类型应在一个单独的限界上下文中进行维护。提供标准类型的聚合时,应保证这些聚合的不变性。

持久化值对象

拒绝由数据建模泄露带来的不利影响

数据建模是次要的,应根据领域模型来设计数据模型,而不是根据数据模型来设计领域模型。

领域服务

表示动作和操作,描述无状态的活动。当领域中的某个重要的过程或转换操作不是实体或值对象的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为Service。定义接口时要使用领域语言,并确保操作名称是Ubiquitous Language中的术语,且应使Service成为无状态的。

过度地使用领域服务将导致贫血领域模型,即所有的业务逻辑都位于领域服务中,而不是实体和值对象中。

好的服务的特征

  • 与领域相关的操作不是实体和值对象的一个自然组成部分;
  • 接口是根据领域模型的其他元素定义的;
  • 操作是无状态的。

可将服务划分到应用层、领域层、基础设施层中,以某资金转账应用服务为例:

层名称 服务名称 功能
应用层 资金转账应用服务 获取输入;发送消息给领域层服务,要求其执行;监听确认消息;决定使用基础设施服务来发送通知。
领域层 资金转账领域服务 与必要的Account(账户)和Ledger(总账)对象进行交互,执行相应的借入和贷出操作;提供结果的确认(允许转账或拒绝转账)
基础设施层 发送通知服务 按照应用程序的指示发送电子邮件、信件或其他信息

对象之间的关联

使关联易于控制的手段:

  • 规定一个遍历方向。
  • 添加一个限定符,以便有效的减少多重关联。
  • 消除不必要的关联。

领域对象的生命周期

管理领域对象的两个难点:

  • 在整个生命周期中维护完整性;
  • 防止模型陷入管理生命周期复杂性造成的困境中。

领域事件

领域事件是领域专家所关心的发生在领域中的一些事件,它是领域模型的组成部分,表示领域中所发生的事情。 通常将领域事件用于维护事件的一致性,领域事件的讨论语境通常是跨系统跨领域上下文的情况。

聚合

将实体与值对象在一致性边界内组成的对象。

聚合的关键实现技术

  • 告诉而非询问(Tell,Don‘t ask)
  • 迪米特法则(Law of Demeter)

原则:在一致性边界之内建模真正的不变条件

聚合边界之内的所有内容组成了一套不变的业务规则,任何操作都不能违背这些规则。边界之外的任何东西与该聚合都是不相关的。

原则:设计小聚合

原则:通过唯一标识引用其他聚合

通过标识引用使多个聚合协同工作

应优先考虑通过全局唯一标识来引用外部聚合,而不是通过直接的对象引用。

建模对象导航性

通过标识引用并不意味着我们完全丧失了对象导航性。可以使用资源库来定位其他聚合,这种技术成为失联领域模型;还可以在调用聚合行为的方法之前,使用资源库或领域服务来获取所需要的对象。

可伸缩性与分布式

当在一个聚合中使用标识引用而非直接引用其他聚合时,便可以大规模地对聚合进行持久化。通过持续地对数据存储进行再分配,几乎可以得到无限的伸缩性。 在一个核心域中,通常存在多个限界上下文,使用表示引用使得我们可以将分布式的领域模型关联起来。

原则:在边界之外使用最终一致性

任何跨聚合的业务规则都不能总是保持处于最新状态。通过事件处理、批处理或者其他更新机制,我们可以在一定时间之内处理好他方依赖。 一个聚合方法所发布的领域事件及时地发送给异步的订阅方,使得每个订阅方都在单独的事务中进行操作,也即满足了“在一次事务中只修改一个聚合实例”的原则。

优先使用值对象

应尽量将根实体所包含的其他聚合建模成值对象,而不是实体。

使用迪米特法则和“告诉而非询问”原则

迪米特法则:强调了“最小知识”原则,客户端对象应尽量少地直到服务对象的内部结构。 告诉而非询问原则:一个对象不应该被告知如何执行操作。

乐观并发

避免依赖注入

工厂

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

  • 类是一种类型。它不是任何相关层次结构的一部分,而且也没有通过接口实现多态性。
  • 客户关心的是实现。
  • 客户可以访问对象的所有属性。
  • 构造并不复杂。

领域模型中的工厂

使用动机:将创建复杂对象和聚合的职责分配给一个单独的对象,该对象本身并不承担领域模型中的职责,但依然是领域设计的一部分。工厂应提供一个创建对象的接口,该接口封装了所有创建对象的复杂操作过程,同时,它并不需要客户区引用那个实际被创建的对象。对于聚合来说,我们应一次性的创建整个聚合,并且确保它的不变条件得到满足。

聚合根中的工厂

好处:

  • 有效地表达限界上下文中的通用语言
  • 减轻客户端在创建新聚合实例时的负担;
  • 确保所创建的实例处于正确的状态。

领域服务中的工厂

资源库

通常将聚合实例存放在资源库中,之后再通过该资源库来获取相同的实例。聚合类型一般和资源库之间存在着一对一的关系。

面向集合资源库

精要:一个资源库应该模拟一个Set集合。无论采用什么类型的持久化机制,我们都不应该允许多从添加同一个聚合实例。另外,当从资源库获取到一个对象并对其进行修改时,我们并不需要“重新保存”该对象到资源库中。

实现面向集合资源库的目标应该是设计并实现一个类似于HashSet的面向集合资源库,但是采用的是真正的持久化数据存储而非内存的java.util.HashSet。实现手段有:

  • 隐式读时复制(Implicit Copy-on-Read):在数据存储中读取一个对象时,持久化机制隐式地对该对象进行复制,在提交时,再将该复制对象与客户端对象进行比较。
  • 隐式写时复制(Implicit Copy-on-Write):持久化机制通过委派来管理所有被加载的持久化对象,委派对象跟踪发生在真实对象上的改变,并标记为dirty,事务提交时,该事务会检查所有dirty对象并持久化其修改。

面向持久化资源库

如果持久化机制不支持对对象变化的跟踪,无论是显式还是隐式的,那么采用面向集合资源库便不再适用了。 面向持久化资源库是一种基于保存操作的资源库,每次新建聚合或修改聚合之后,我们都需要调用资源库中的save()方法或者与之类似的方法。

资源库 vs DAO

两者常被当作同义词看待,都提供了对持久化机制的抽象。 资源库和DAO是不同的。一个DAO主要从数据库表的角度来看待问题,并且提供了CRUD操作;资源库和数据映射器则更加偏向于对象,因此通常被用于领域模型中。

集成限界上下文

一个项目中通常存在多个限界上下文,并且我们需要在它们之间进行集成。

集成基础知识

集成限界上下文方式

  • 在一个限界上下文中暴露应用程序编程接口(API),然后在另一个限界上下文中通过远程过程调用(RPC)的方式访问该API。
  • 使用消息机制。
  • 使用RESTful HTTP。

跨系统边界交换信息

跨系统边界信息形式有XML、JSON等。将外部对象直接当作本地对象是存在危险的,而这也违背了DDD的战略设计原则。可以使用端口适配器将本地系统的领域模型和外部模型隔离开来。

应用程序

用户界面

用户界面可能需要渲染来自多个聚合实例的属性数据,但是在提交修改时,却只能一次修改一个实例。

如果你的程序提供了REST资源,那么你便需要为领域模型创建状态展现以供客户端使用。有一点非常重要:我们应该基于用例来创建状态展现,而不是基于聚合实例。从这一点来看,创建状态展现和DTO是相似的,因为DTO也是基于用例的。然而,更准确的是将一组REST资源看作一个单独的模型——视图模型(View Model)或展现模型(Presentation Model)[Fowler,PM]。

应用服务

不要将领域服务与应用服务混杂在一起了。在应用服务中,我们并不会处理业务逻辑,但是领域服务却恰恰是处理业务逻辑的。

Releases

No releases published

Packages

No packages published

Languages