Skip to content

yeaze/ddd-go-demo

 
 

Repository files navigation

项目介绍

本项目为DDD-领域驱动程序设计golang语言的demo版本,希望通过此项目让开发者快速了解DDD的样貌。 通过本项目您可以快速了解DDD中目录结构的设计,DDD中的类型,例如entity, aggregate, vo, DDD中app层处理的CQE对象等等。 项目中的目录结构是作者经过各种资料的学习及自己的思考总结认为比较通用的结构设计,在实际应用的过程中可以根据自身的需求做出适当的改变。 本项目会以常见的抽奖业务场景带领大家来了解DDD,在抽奖业务场景中主要分为4个领域,assets-资产领域,inventory-库存领域,lottery-抽奖领域,risk-风控领域。如果大家觉得项目对自己起到了一定的帮助,麻烦给作者点个小星星!谢谢!

目录结构树形图及简介

.
├── application 应用层,负责业务流程编排(业务流程涉及到的多个领域服务方法编排)
│   └── services 存放所有service文件
│       ├── assets.go 资产相关service
│       └── lottery.go 抽奖相关service
├── domain 领域层(核心),包含业务功能的所有逻辑
│   ├── assets 资产领域
│   │   ├── aggregate 聚合根存放位置,若想了解聚合根相关概念请查阅快速入门
│   │   │   └── coin.go 金币相关聚合根文件
│   │   ├── entity 实体存放位置,若想了解实体相关概念请查阅快速入门
│   │   │   ├── coin.go 金币相关实体
│   │   │   └── prize.go 实物奖品相关实体
│   │   ├── services 领域服务层
│   │   │   ├── coin.go 金币相关服务方法
│   │   │   └── prize.go 实物奖品相关服务方法
│   │   └── vo 值对象存放位置,若想了解值对象相关概念请查阅快速入门
│   │       └── prize.go 实物奖品相关值对象
│   ├── inventory 库存领域,此处因是demo,对于一些目录结构做了一些省略,详细目录结构以assets领域为准
│   │   └── services 
│   │       └── prize.go
│   ├── lottery 抽奖领域
│   │   ├── entity
│   │   │   └── award.go
│   │   ├── services
│   │   │   └── lottery.go
│   │   └── vo
│   │       └── award.go
│   └── risk 风控领域
│       └── services
│           └── lottery.go
├── infrastructure 基础设施层,向其他层提供通用的技术能力(比如工具类,常用基本配置,发送mq消息,访问数据库等等)
│   ├── dao dao层,负责持久化数据的CRUD操作
│   └── mq mq层,负责向消息队列发送消息。
│       ├── kafka
│       └── rocketmq
└── interfaces 用户接口层,负责向用户显示信息(比如获取首页的商品数据)和解释用户命令(用户发起抽奖)
    ├── grpc.go 接受grpc请求,然后调用application层方法处理。
    ├── http.go 接受http请求,然后调用application层方法处理。
    └── mq_consumer.go 接受mq消息,然后调用application层方法处理。

抽奖业务流程图

image

业务领域设计介绍

在本业务场景中,主要分了assets-资产领域,inventory-库存领域,lottery-资产领域,risk-风控领域。

  • assets资产领域: 管理用户资产,例如增加用户金币,给用户发放实物奖品,查询用户相关资产等等。
  • inventory库存领域: 管理奖品库存,给用户发放奖品时,需要通过库存领域判断奖品是否充足,以及发奖失败时,需要回补库存。
  • lottery抽奖领域: 管理用户抽奖,仅关注用户根据产品策略可以抽中什么奖品,不关心库存是否充足。
  • risk风控领域: 业务风险检测,用户进行抽奖前,首先需要过风控检查,拦截不正常用户。

层级调用关系图

image

更多DDD相关知识

以下知识,本人尽可能用比较简短话语进行了总结,意在帮助大家快速入门。

DDD简介

DDD-Domain-Driven Design全程领域驱动程序设计,是一套综合软件系统分析和设计的面向对象建模方法。在DDD中,一旦理解了软件需要解决的业务领域问题,那么你所要做的就是建立一个分层架构,业务逻辑被分成两个不同的模块,核心领域逻辑(domain service)和应用程序逻辑(application service)的实现。DDD的本质可以归纳为两个字,分与合。 所谓分指的是对业务需求进行领域拆分,领域中又细分为实体,值对象,聚合根,领域中的每个对象需职责单一并且明确。所谓合具体有3点:1. 领域对象针对业务场景可以聚合形成聚合根,聚合根(aggregate)的作用请参考下面的详细介绍。2. 某些需求需要领域中多个对象参与,一步步完成特定功能,此时体现在domain的service将多个领域对象方法进行组合。3. 某些需求需要多个不同的领域共同参与完成,此时application层将多个不同领域的方法进行组合。

目录结构介绍

  • Infrastructure层 : 即基础实施层,向其他层提供通用的技术能力,比如工具类,通用方法,第三方库类支持,常用基本配置,数据访问等等。
  • Interfaces层 : 即接口层,负责向用户显示信息和解释用户命令,请求应用层以获取用户所需要展现的数据(比如获取首页的商品数据),对外可暴露grpc方法,或者http方法等提供外部访问。
  • Application层 : 即应用层,相对于领域层应用层是很薄的一层, 主要作用是负责业务流的编排,即结合多个领域的服务完成某项操作。应用层仅仅负责编排,不应该存在具体的业务逻辑。
  • Domain层 : 即领域层,DDD中的核心层,维护所有的业务逻辑,领域层中主要包含了领域模型(vo, entity, aggregate ),repo(仓储层, 负责操作领域对象,和dao层不同,后面会单独介绍),service,service层对外提供领域服务供应用层调用。

如何划分领域

领域一词主要有以下两个意思:1. 一国主权所达之地。2. 学术思想或社会活动的范围。从领域的意思上可以看出领域重在范围的界限。那么DDD中的领域又是什么意思呢?其实DDD中的领域也并没有什么特别之处,它只是被界限在指定的业务需求之中,简单一点理解领域即问题域,不同领域需要解决的问题不同。 那么我们在拿到业务需求后,又如何合理的将需求拆分为多个领域呢?网上大部分的观点是最好有一个领域专家来进行子域的拆分。有权威的领域专家当然是最好的啦,如果没有的话可以拉上同组的小伙伴,大家一起讨论下如何拆分比较合理,拆分必须满足以下原则:领域之间不可相互调用,领域中只需要解决领域内部的问题,如果划分的领域间存在强依赖关系那么就应该考虑领域的划分是否合理。在实际的开发过程中,随着业务规模的不断扩大可能需要随时进行调整优化,有可能需要对已划分的领域继续拆分。

模块

有小伙伴可能会问,一直都在说领域如何如何,那么领域在代码层面上的表现形式是什么样的呢?其实领域在代码层面上的组织形式就是模块,在DDD中,模块和领域的关系应该是一对一的,不同模块所解决的问题肯定是互不相干的,进行模块设计时需要保证高内聚低耦合。

界限上下文

界限上下文是一个比较抽象的概念,理解界限上下文有助于我们合理的划分领域,也有助于我们理解领域内的聚合根。界限上下文可以拆分为界限和上下文分开理解,界限是指不同事物的分界,上下文可以理解为环境,在开发过程中环境可以理解为场景。在DDD中限界上下文是业务上下文的边界,在该边界内,当我们去交流某个业务概念时,不会产生理解和认知上的歧义(二义性),限界上下文是统一语言的重要保证。关于限界上下文有一个非常形象的定义:细胞之所以会存在,是因为细胞膜定义了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。以电商业务举例,在订单系统和销售系统中存在商品对象,商品包含各种信息,例如价格,名称,规格等等,在这两个系统中商品的概念是明确没有歧义的。但是如果和物流系统进行交互时仍然使用商品对象就显得不那么合适了,因为物流系统关心的只是订单号,即只关心某个订单的物流情况,至于运输的是什么商品,价格和尺寸是什么样的,这些都不应该是物流系统需要关心的。界限上下文这个概念的核心宗旨是让开发在特定的业务场景下合理的划分各个功能模块,各个功能模块内部也需要根据业务场景进行合理的划分。

界限上下文和领域的关系

不同领域是有界限的,并且领域也是根据特定业务场景(环境)形成的,所以不同领域处在不同的界限上下文中。那么领域中还会包含多个界限上下文么?其实领域中根据不同的业务场景也会形成不同的界限上下文。在领域内部,每一个聚合根可以理解为一个界限上下文,聚合根在后面会向大家介绍。

界限上下文之间的关系

在实际的开发过程中,往往需要多个界限上下文共同参与到业务逻辑中,这些界限上下文可能在一个领域内,也可能在不同领域中。界限上下文之间也会存在一些关联关系,接下来将介绍界限上下文之间存在的关系。

  • 合作关系(Partnership):两个上下文紧密合作的关系,一荣俱荣,一损俱损。
  • 共享内核(Shared Kernel):两个上下文依赖部分共享的模型。
  • 客户方-供应方开发(Customer-Supplier Development):上下文之间有组织的上下游依赖。
  • 遵奉者(Conformist):下游上下文只能盲目依赖上游上下文。
  • 防腐层(Anticorruption Layer):一个上下文通过一些适配和转换与另一个上下文交互。
  • 开放主机服务(Open Host Service):定义一种协议来让其他上下文来对本上下文进行访问。
  • 发布语言(Published Language):两个限界上下文之间翻译模型所需要的公用语言,通常与开放主机服务一起使用。
  • 大泥球(Big Ball of Mud):混杂在一起的上下文关系,边界不清晰。
  • 另谋他路(SeparateWay):两个完全没有任何联系的上下文。

设计模型介绍

接下来介绍DDD中的4种常见的设计模型及优缺点,具体如下:

  • 失血模型 model里只有属性的getter/setter方法,所有的业务逻辑完全由service层实现。 优点:model仅包含状态,无业务逻辑。 缺点:service层负责维护所有业务逻辑,service层相当厚重,随着业务复杂性的增加维护成本可能越来越高。

  • 贫血模型 model中除了属性的getter/setter方法,还包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到service层, 应用在DDD中的效果是:domain service调用repo层的方法获取到相应的model,model执行方法完成领域逻辑,然后再调用repo将model保存下来。 优点:model中包含了一些领域逻辑,降低了service层的复杂度,各层代码分布相对比较均匀。 缺点:model中部分比较紧密依赖的持久化逻辑被分到了service层,显得不够面向对象,举个例子来说就是model本身没有方法可以将自己持久化保存。

  • 充血模型 充血模型和上面的贫血模型差不太多,所不同的就是如何划分业务逻辑,即认为,绝大多业务逻辑都应该被放在model里面(包括持久化逻辑),而service层应该是很薄的一层,仅仅封装事务和少量逻辑,不和数据层打交道。应用在DDD中的效果是:model中包含对应的数据操作方法,相关的作方法调用repo层的方法来完成数据的持久化,查询等,domain service调用model对应的方法获取数据,model执行业务逻辑方法, 最后调用model.Save方法将信息保存(根据实际业务场景,model在执行业务逻辑方法时可能已经产生了数据的变化,此时不用显式调用Save方法)。 优点:更加符合面向对象的原则,model的行为和状态内聚在了一起,便于扩展及维护。 缺点:model层会稍微重一些,service层很薄,有些时候可能仅仅充当faced的角色。

  • 胀血模型 基于充血模型的缺点,有同学提出,干脆取消service层,只剩下model层。 优点:减少了层数,也算符合面向对象的原则。 缺点:model层非常重,可扩展性比较差,随着业务复杂性的增加,维护成本可能越来越高。

  • 总结 上面的四种模型各有利弊,那么在实际项目中我们该如何做选择呢?大多数情况下失血模型和胀血模型我们是不会去考虑的,因为这两种模型都有一个共同的缺点,就是代码的分布比较失衡,不利于后续的扩展和管理。那么剩下的两种模型我们该如何选择呢?个人认为在一些简单的业务场景中可以使用贫血模型,例如后台系统,基本就是对数据库的CRUD操作。一些复杂的业务场景可以使用充血模型,更加面向对象的编程有利于代码的后续迭代及维护,这些仅代表我个人观点。在实际开发过程中,大家可以根据业务情况灵活的来选择模型。

application层处理的CQE对象

在DDD中或者说在软件开发的过程中,应用程序需要处理的事情基本上可以归纳为三类。这三类分别是command-命令,query-查询,event-事件,简称CQE。

entity

entity即实体,在DDD中实体又是什么呢? DDD中要求实体是唯一的且可持续变化的。意思是说在实体的生命周期内,无论其如何变化,其仍旧是同一个实体。唯一性由唯一的身份标识(id)来决定的。可变性也正反映了实体本身的状态和行为。例如mysql中一行记录的的model可以算作实体,需注意除了上述情况,只要满足实体的条件都可算作实体,实体中的属性有可能来自多个表。

vo

vo-value object即值对象,区别于实体,值对象不存在唯一身份标示,在领域中并不是每一个事物都必须有一个唯一身份标识,对于某些对象,我们更关心它是什么而无需关心它是哪个。比如说用户的收货地址,可能用户的收货地址是存在db中的,并且有一个id与之对应,但是在处理业务的过程中我们可能并不关心收货地址记录的id是什么,我们更加关注收货地址具体是什么,所以可以将收货地址定义为值对象。

aggregate

aggregate-聚合根,从字面意思来理解就是对象的组合,但是又不仅仅是对象的组合那么简单,聚合的主要的目的是用来封装业务和保证聚合内领域对象的数据一致性。举个例子来帮大家更好的理解聚合根,比如用户通过某种手段可以获得现金作为奖励,那么用户的资产可以保存在db中,用户的资产记录可以作为一个实体存在,用户获得现金奖励的时候需要记录获取方式,即获奖流水信息,该信息同样也可保存在db中作为一个实体的存在。在获奖上下文中,用户的资产需要和流水信息一起产生改变,那么其实可以将这两个实体包装成一个聚合根,聚合根中提供发奖方法(修改资产的同时会产生流水记录),用来保证数据的一致性。需注意聚合根中的对象不一定必须都是实体,也有可能是值对象。简单一点来讲聚合根是根据业务的不同场景而形成的。

事件风暴

上面介绍了各种DDD中的领域模型,那么在实际的开发过程中,有没有什么通用的方法可以快速的整理出领域模型呢?这里想向大家简单的介绍下如何使用事件风暴来整理需求中的领域模型。总的来说分三步,第一步: 罗列出需求中的所有会发生的事件,通过主语加定语的形式来进行整理,以电商业务为例可以整理出的事件有订单(主语)已创建(定语), 商品已创建,库存已增加等等。第二步:把事件中的名词都提取出来,例如订单,库存,商品等等,然后将名词归类为entity和vo。第三步:根据业务需求将entity和vo组织在一起形成聚合根。

domain event

domain event即领域事件,可以理解为领域内部发生的某些改变外部需要感知到,此时领域内可以通过某些手段发布事件供外部发现。一般情况下事件都是异步的,即领域内并不关注事件的消费方是谁,消费状态是如何的,所以常见的事件实现方法就是通过mq。以常见的电商业务为例,比如用户购买的商品到货了,这时我想通过用户绑定的微信通知到用户,一般来说有两种方式,一是同步调用微信通知的接口,但是如果需要加入站内通知或者短信通知,难道每增加一种通知方法都要去修改代码调用对应的接口?此时比较具有扩展性的做法是将event放入mq中,业务本身不关注消费方是谁,如果增加了通知类型那么增加消费端或者在消费端实现具体逻辑就好了。

factory

工厂模式在设计模式中应该还是比较常见的模式,不了解的小伙伴可以先自行了解下。在DDD中如果聚合根对象的形成比较复杂,可以将聚合根对象的生成封装在工厂中,外部不需要关注对象生成具体的实现细节。

domain层

domain层是DDD中的核心,层中包括service-对外提供领域服务,repository(简称repo)-提供领域对象的CRUD操作。开发过程中所有的业务逻辑都将在domain层中实现。

dao

dao-数据访问对象并不是DDD中特有的概念,dao提供数据基本的CRUD操作。在DDD中dao存在于基础设施层中,有小伙伴可能会有这样一个疑惑,上面的domain层中的repo和dao有什么区别呢?其实如果按照软硬件来进行分类的话,domain层应该属于软件层面,domain层应只关注业务逻辑,而不需要关心数据存储在什么硬件上面。所以在DDD中关于数据的真正操作(和硬件层打交道)就放在了基础设施层中的dao中。repo层其实是需要调用dao层来进行真正的数据操作的。repo关心的是领域对象,dao关心的是DO(data object)数据对象,repo除了操作领域对象还有一个重要功能,就是协调DO和领域对象。个人观点:如果业务需求很简单,并且数据源将来也基本不可能发生改变,这种情况下是可以省略dao的,关于数据的真正操作直接放repo中,这样做也避免了DO和领域对象之间的转换,简化了逻辑。尤其对golang语言而言,如果一定要引入DO,目前来看转换的逻辑必须要自己实现...

DDD的优缺点思考

  • 优点: 1. 使用DDD可以提高代码的扩展性,可读性,可维护性,在比较复杂的项目中使用DDD的收益是非常不错的。 2. 在一个项目中可能存在多个领域,使用DDD形成多个领域后,若需要进行项目或者服务的拆分将变得更加方便,因为所有的核心业务逻辑都集中在domain中。3. DDD是建立在面向对象的基础之上,扩展性更佳。 4. 各个层级职责明确,软硬件交互分离。

  • 缺点: 1. 层级较多,会增加些许开发量。 2. 如果业务逻辑比较简单,使用DDD带来的收益比较小。 3. DDD比较适合做业务开发,不太适合应用于框架的开发上,目前暂时还没有看到哪款开源框架是使用DDD的思想来实现的。 4. 涉及的概念比较多,入门门槛比较高。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Go 100.0%