DDD领域驱动设计的项目实践

Domain-Driven Design

DDD的概念众所周知,各个文章中的基本概念也都大同小异。这里就不再累述了。DDD最大的好处是使用领域通用语言(UBIQUITOUS LANGUAGE)将原先晦涩难懂的业务通过领域概念清晰的显性化表达出来。写这篇文章的目的在于学习怎么使用DDD来降低应用的复杂度,增加框架的可扩展性。本文主要阐述了我在项目中的思考过程和架构实现。

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

我们从以下一些方面进行些讨论以帮助我们完成DDD的具体实践。

1.DDD分层架构

DDD领域驱动设计的项目实践_第1张图片
分层架构
分层 描述
UserInterface UI层,负责数据接收的消息处理、页面展示的数据封装。
Application 展示层与领域层的中间层,定义软件要完成的所有任务,提炼总结表现层的需求,不应随着上层频繁变化而变化。向下调用领域层(领域对象或领域服务)协调领域层进行工作,应用层不包含业务逻辑,但包含流程控制逻辑。
Domain 业务逻辑的核心层,领域模型都处在这一层 。领域层的重点应该为如何表达领域模型。
Infrastructure 为其他层提供支持,Repository,Config,Common和message都在这一层,通过基础设施层对上游提供功能、屏蔽实现细节。

  分层最大的意义是在架构的层面上来进行职责的拆分,分离关注点,每一层都确定独立的职责,分层内部保持高度内聚性,让每一层只解决该层关注的问题,从而将复杂的问题简化,它们只对下层进行依赖,实现高内聚低耦合、职责分离的思想。
  应用层代表。
那么应用层代表什么,就是代表系统;所以, 我们要明白人与系统的关系;人使用系统,系统提供外部功能,系统内部有领域模型;

  • 内聚
    每一个分层都应该具有良好的内聚性,这个在UserInterface、Application、Infrastructure都没有太大的问题。但在Domain层中存在数据边界和行为边界不一致的问题,我们通过类将行为和其紧密耦合的数据封装在一起。但是在复杂的业务场景下,行为往往跨越多个领域对象。

这个问题,DDD通过DCI架构(Data、Context和Interactive三层架构),显式的用role对行为进行建模,同时让role在context中对应的领域对象进行绑定(cast)来解决。

待扩展.......

  • 耦合

在分层架构中,我们将领域模型和业务逻辑分离出来,并减少对基础设施、用户界面甚至应用层逻辑的依赖,因为它们不属于业务逻。将一个复杂的系统分为不同的层,每层都应该具有良好的内聚性,并且只依赖于比其自身更低的层。   --Evans

由于DDD分层架构这种向下依赖的模式使得整个框架对于Infrastructure层过度依赖,这不是我们所期望的,我们希望能在Application和Domain层更多的决定领域的功能和流程。所以根据DIP(依赖倒置)原则, 高层模块和低层模块都应该依赖于抽象,为了让模型的逻辑定义更为内聚,我们可以把抽象放在上层(Application、Domain),Infrastructure提供实现。

例如在现在的项目中,我们在Domain层中定义IRepository、ISender、IListener等行为接口,在Frameworks and Drivers层(The Clean Architecture)实现接口,通过注入的方式提供给Domain层。这样能够使依赖的核心层更加内聚,同时对外层的扩展预留出了足够的空间。

(同样的操作我们还尝试的用在对领域层中,将领域对象抽象化,使领域服务依赖于领域对象的抽象,将真正的的领域对象交给更外层去实现,来满足我们架构对于相似业务的扩张能力。但这种做法是极具风险的,不同业务的差异化和领域对象抽象的能力都是极难把握的。总的来说,我们所期望的方向是:1.抽象能够涵盖所有业务。2.业务的差异化只在抽象的实现层被应用。只有满足以上2点,我们的想法才有可能被实现。)

Any problem in computer science can be solved with another layer of indirection. But that usually will create another problem.    --David Wheeler


聊完了高低层模块的依赖,我们再来聊下同层之间的依赖。

分层架构的一个重要原则是每层只能与位于其下方的层发生耦合,有些书中还有严格分层架构和松散分层架构两种区分。两者的区别在于能否对非直接下层发生耦合。但有一点是相同的,同分层之间的耦合都是不被允许的。但问题是有些时候,实际情况使它并不是容易被实现的。
例如在我们的项目中,为了满足高吞吐的业务需求,我们选用了Actor模型和AKKA多线程框架来构建我们的架构。这种响应式的异步模式上层无法及时获得下层的反馈,这使上层对下层的协调、分配变得异常艰难。所以我们需要在不同的分层中都创建Actor进行交互,通过对sender的回件来进行解耦。(TODO:图)

Actors and DDD: A Perfect Match

To quote Alan Kay, "The Actor model retained more of what I thought were the good features of the object idea."
 Also from Kay: "The big idea is messaging." 
In creating a ubiquitous language, developers can focus on the actors as being the objects or the components, the elements of the domain model, and the messages sent between them.

One reactive service is no service; reactive services come in systems. 
And so what developers are ultimately trying to accomplish is to build full systems, not just single services.
By emitting domain events to other bounded context or microservices, developers can accomplish this more easily.

Actor模型对于DDD的使用还是有很多帮助的,他们都有相同的对象理念,同时,这种响应式架构使领域事件到其他的边界上下文或微服务变得更容易。


The Clean Architecture

经过一些分层、抽象,The Clean Architecture是我们项目期望的目标。

  1. Independent of Frameworks. The architecture does not depend on the existence of some library of feature laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints.
  2. Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element.
  3. Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.
  4. Independent of Database. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
  5. Independent of any external agency. In fact your business rules simply don’t know anything at all about the outside world.

关于整洁架构...等有时间再写了...
The Clean Code Blog by Robert C. Martin (Uncle Bob)

2.领域建模

领域建模是整个DDD中比较核心的步骤,大部分DDD的工程都是基于领域建模展开的。这里同样不再对实体,值对象等做过多的名词解释,只来记录一次领域建模的过程。


DDD领域驱动设计的项目实践_第2张图片
DDD领域建模过程 .png

这是一个关于交易平台报价行情系统的部分需求:
1.交易员提交报价 2.交易员选择[群组]提交报价
3.交易核心处理报价 4.交易核心下发[报价报告] 5.交易核心下发[报价行情消息]
6.行情系统订阅交易核心相关消息
7.行情系统收到[全市场][报价报告],开始计算报价行情:
a.根据[报价报告][计算]出[公有报价行情]。b.根据[公有报价行情][聚合]出[公有聚合报价行情]。c.根据[公有报价行情]为[机构]生成[私有报价行情]。d.根据[私有报价行情][聚合]出[私有聚合报价行情]。
8.行情系统收到[群组][报价报告],开始计算报价行情:
a.根据[报价报告]为[群组]包含的[机构][计算]出[私有报价行情]。b.根据[私有报价行情][聚合]出[私有聚合报价行情]。
9.行情系统收到[公有报价行情消息],开始计算报价行情:
a.将[公有报价行情消息][转化]成[公有报价行情]。b.根据[公有报价行情][聚合]出[公有聚合报价行情]。
10.行情系统收到[私有报价行情消息],开始计算报价行情:
a.将[私有报价行情消息][转化]成[私有报价行情]。b.根据[私有报价行情][聚合]出[私有聚合报价行情]。

第一步:分析需求,定义关键类,描述业务流程
1.从需求中抽象出我们所关心的类
Book:报价簿,报价行情形象话的表现
Page:报价簿中记录内容的抽象类
Institution:.........

2.建立类的大致内容、行为和关系


DDD领域驱动设计的项目实践_第3张图片
报价行情类图.png

上图只是对类的简单定义,项目中最后落地一定不可能如此简单,需要进行反复的细化工作。

3.描述大致的业务活动图


DDD领域驱动设计的项目实践_第4张图片
报价行情计算流程活动图.png

第二步:划分主要构成
我们必须在这步中识别出模型中的实体、值对象,定义出核心域、子域并理清关系,划分应用层和服务层,确定限界上下文边界。
本需求寻找核心有两种方向,一种以行情计算和聚合为核心,机构、群组等作为独立子域;一种以行情拥有对象(全市场、机构)为核心,行情计算、聚合作为支撑子域;第一种偏向横向分层,第二种偏向纵向分层。按实际情况,我们选择了第二种方式,围绕Owner(全市场和机构)展开它们的行情计算、价格预警、价格变化、单机构等等。同时,上面的类图、流程图都需要调整,使依赖尽量向核心(Owner)偏移。

核心域:Owner。生成报价行情的对象,报价行情的所有者。
子域:报价簿子域、资产子域、群组子域、预警子域、监控子域、报价明细子域。
实体:机构、群组、全市场、报价单、报价簿、报价明细、资产信息。
值对象:报价簿规则、报价簿ID。
领域服务:机构服务、群组服务、行情计算服务、行情聚合服务、报价预处理服务、仓库服务。

当领域中的某个操作过程或转换过程不是实体或值对象的职责时,我们便应该将该操作放在一个单独的接口中,即领域服务。请确保该服务和通用语言时一致的;并且保证它是无状态的。

限界上下文

一个领域本质上可以理解为就是一个问题域,只要是同一个领域,那问题域就相同。所以,只要我们确定了系统所属的领域,那这个系统的核心业务,即要解决的关键问题、问题的范围边界就基本确定了。

第三步:聚合
根据前面的识别出来的各类对象初步构造出模型。

DDD领域驱动设计的项目实践_第5张图片
报价行情构建模型.png

第四步:细化模型
反复考虑业务,基于性能、扩展等方面因素,进行模型的细化工作,反复迭代。
DDD领域驱动设计的项目实践_第6张图片
报价行情类图 -2.png


3.CQRS和EDA


DDD的设计落地

待续....



Ticket

DDD + Actor的一些思考

Actor是Actor.class模型的核心,怎样在DDD的架构中使用Actor?

  • 应用服务和领域服务分别创建成Actor,行为和流程责任划分,通过两者的交互推动业务流程。

Actor的能力和Service相似,服务是无状态的,但状态却是Actor模型的特点。一个无状态的领域服务一般使用单例模式,而当一个类有了状态,就好像一个物体有了生命,“我是谁?我在哪?”这些也都成了它所需要关心的问题。我们需要维护服务列表、制定路由规则、实现服务发现功能...写到这,我们可以发现这些和微服务的服务治理非常相似。所以,我们可以建立我们的“注册中心”封装select,create,actorof等Akka的操作来实现服务发现,还能在里面提供限流、熔断、合并等等扩展。

  • message-->domain event。在Actor中message的发送都是通过指定下游Actor进行的,Actor间直接message交互会增加服务间耦合,DDD中更希望通过DomainEvent使用观察者模式实现对流程和分层间的解耦。所以在AKKA项目的中,我们可以实现akka.event.japi.EventBus,各个Actor围绕EventBus进行sub/pub。
  • 应用层和领域层屏蔽actor的create,select,remote,shard等操作。应用层关注流程;领域层关注行为;将其他的关注交给基础设施层或其他分层解决,Infrastructure层提供Factory和Adapter,使用适配模式接入AKKA。AKKA只是工具,实现工具不应影响业务流程,尽量减少AKKA对于领域的可见度。
  • 领域服务Actor和Entity:Entity和Actor都不要直接将属性直接暴露给别人,通过方法提供自身行为。

Bad programmers worry about the code. Good programmers worry about data structures and their relationships.    --Linus Torvalds


面向对象
DDD是一种方法论,它的本质还是面向对象的思想。DDD在OOAD的基础上提炼演进出了一套架构设计理论。帮助我们,使我们能更容易的以面对对象的思想来设计工程。DDD的设计也需要遵循OOAD的SOLID原则。


有之以为利,无之以为用。

参考资料

Effective Aggregate Design by Vaughn Vernon
The Clean Code Blog by Robert C. Martin (Uncle Bob)
Reactive-Systems-Akka-Actors-DomainDrivenDesign
复杂度应对之道 - COLA应用架构

你可能感兴趣的:(DDD领域驱动设计的项目实践)