DDD系列-战术设计-实践
2022-12-05 01:32:46    261    0    0
weibo-007

DDD系列-请假系统案例

项目基本信息介绍

请假申请人填写请假单提交审批,根据请假人身份,请假类型,和请假天数进行请假审批规则校验,再根据审批规则逐级递交上级领导审批,批核通过则完成审批,批核不通过则退回申请人。

另外,根据考勤规则,核销请假数据后,对考勤数据进行系统分析,输出考勤统计。

战略设计

战略设计阶段需要有领域专家,业务需求方,产品经理,架构师,项目经理,开发经理,和测试经理一同参与,主要采用事件风暴的方式进行。

产品愿景

产品愿景是对软件产品进行顶层价值设计,对目标用户,核心价值,差异化竞争等信息达成一致,避免产品设计和建设偏离方向。

最终形成的产品愿景图,有了这张图我们可以知道我们这个产品的定位。

用户故事分析

在完成产品愿景分析,明确产品建设方向和目标后,我们就可以针对产品的具体业务场景开始场景分析了。场景分析是从用户视角出发,探索业务领域中的典型场景。这里列举两个用户故事

请假

用户故事名称:请假

参与者:请假人

主要功能:

1)请假人登陆系统,从权限服务那获取请假人信息和权限信息,完成登陆认证。

2)创建请假单,打开请假页面,选择请假类型(事假,病假,婚假等)和起始时间,录入请假信息,创建并保存请假单,提交请假审批。

3)提交审批,获取审批规则,根据审批规则(事假+1上司审批,婚假+2上级审批规则等),从人员组织关系中获取审批人,给请假单分配审批人。

审批

用户故事名称:审批

参与者:审批人

主要功能:

1)审批人登陆系统,从权限服务那获取请假人信息和权限信息,完成登陆认证。

2)获取请假单,获取审批人名下待办请假单,选择一个请假单。

3)审批,填写审批意见。

4)逐级审批,如果还需要上级审批,根据审批规则,从人员组织关系中获取审批人,给请假单分配审批人。如果还有上级审批人,重复这个步骤。

5)最后全部审批人完成审批

其实,你不得不承认,文字描述很难让全部的参与者达成一致,所以用户故事最好的呈现方式是采用图文的形式进行阐述。

用户故事分析快速帮我们达成一致,让所有角色都知道我们接下来要做的事情,但是这里并不会过度关系细节,这里的分析不能转成战术分析。

事件风暴

我们会召集到全部的领域专家,产品团队,架构师,对产品进行一次事件风暴,事件风暴的过程参考:http://blog.leanote.com/post/weibo-007/DDD系列-限界上下文 

领域事件

从我们和领域专家的进一步深入沟通中,我们可以发现下面几个领域事件

1)请假单已创建

2)请假单已修改

3)请假单已送审

4)审批已通过

5)审批已驳回

6)审核规则已提交

围绕这几个领域事件,按照每个角色关注的事情进行事件风暴,形成完整的事件风暴图

(这里只列举有限的事件,当把全部角色聚在一起的时候,他们自然会提出自己关系的事情,直到这张图变得非常庞大)

领域建模

领域建模是通过对业务和问题进行分析,找出领域对象以及他们对业务行为和依赖关系,建立领域模型的过程。领域建模是一个收敛过程,主要分成三步

1)第一步提取领域对象,从业务操作或者行为中,抽象并提取领域实体和值对象。

2)第二步是构建聚合,从众多实体中找出聚合根,并且找出聚合根依赖的实体,值对象。

3)第三步是划分界限上下文,根据业务及语义边界等因素,将多个聚合划分到一个业务上下文环境中,确定领域模型的限界上下文边界。

提取领域对象

针对事件风暴的内容,将命令和事件与实体和值对象建立关联关系。通过业务行为和领域事件分析,我们提取产生这种行为和事件的领域对象。

构建聚合

在定义聚合前,先找到聚合根,我们发现请假单,人员具备聚合根的特征。它们有独立的生命周期,有全局唯一的ID。所以我们将请假单和人员定义为聚合根,然后找出聚合根紧密相连的实体和值对象。我们发现,审批意见,审批规则和请假单聚合根紧密相连;组织关系和人员聚合根紧密相连。这个也可以作为限界上下文的划分原则,这里一共划分出三个限界上下文,我们也可以以此来划分微服务。

1)请假上下文

2)人员上下文

3)考勤上下文

战术设计

完成战略设计之后,其实对研发而言,还是不知道怎么写代码。所以这里还有一道鸿沟,战术设计就是解决这道鸿沟的。

服务识别和设计

命令往往是由外部操作后产生的一些业务行为,一般也微服务对外提供的服务能力。我们可以将命令作为服务识别和设计的起点,具体步骤如下

1)由于应用服务主要面向用例,我们可以以此确定微服务的基本功能

2)根据应用服务设计领域服务,定义领域服务

3)根据领域服务的功能,确定领域服务内实体以及实体自生的业务行为,注意,不要让实体变成贫血模型

4)设计实体的基本属性和方法

 

这里以提交审批这个动作为例,主要业务流程

1)根据人员类型,请假类型和请假天数,查询请假审批规则,获取下一步审批人的角色

2)根据审批角色从人员组织关系中查询下一审批人

3)为请假单分配审批人,并将审批规则保存至请假单


DDD典型的分层架构如下,按照这个架构,我们梳理出审核这个业务动作涉及的方方面面。

   

聚合内的对象

在请假单聚合中,聚合根是请假单,请假单经过多级审核后,会产生多条审批意见,为了方便查询,我们可以将审批意见设计为实体。因此请假聚合中包含审批意见实体(记录审批人,审批状态和审批意见)和请假单实体

请假聚合中的值对象包含,人员信息,审批规则等只需要读取的信息,这些信息有一个特点,生命周期在其他聚合中维护。

在人员关系聚合中,我们可以建立人员之间的组织关系。可以通过组织关系找到上级领导。

代码实现

代码结构说明,主要目录结构说明

leave-------------------------------------------#项目名称
├── application---------------------------------#应用层
│   ├── doc.go
│   └── service---------------------------------#应用服务
│       ├── leave_application_service.go--------#请假应用服务
│       ├── login_application_service.go--------#登陆应用服务
│       └── person_application_service.go-------#人员应用服务
└── domain--------------------------------------#领域层
    ├── approval_rule---------------------------#审核规则领域
    │   ├── entity------------------------------#实体/值对象
    │   ├── event-------------------------------#领域事件
    │   ├── repository--------------------------#仓储
    │   └── service-----------------------------#领域服务
    ├── leave-----------------------------------#请假领域
    │   ├── entity
    │   ├── event
    │   ├── repository
    │   └── service
    └── person----------------------------------#人员领域
        ├── entity
        ├── event
        ├── repository
        └── service 

应用层

DDD 中的应用层 这里主要实现的是面向业务动作,负责业务流程编排,通过调用领域层服务完成面向业务的动作

领域层伪代码,一般应用层的代码是PM可以理解的,PM完全可以看懂这里的流程在干的事情。比如提交请假的业务流程

//步骤一:获取审核规则
var leaderMaxLevel = l.approvalRuleDomainService.GetLeaderMaxLevel()
//步骤二:获取审批人
var approver = l.personDomainService.FindFirstApprover()
//步骤三:创建请假单
l.leaveDomainService.CreateLeave(leave, leaderMaxLevel, approver)

领域层

这里分了3个领域,他们分别负责各自领域内的一切事务。

1)审核规则领域

2)请假领域

3)人员领域

实体/值对象

其中领域层的代码就是我们前面战术设计的实体或者值对象组成。比如请假领域的实体和值对象

leave-----------------------------------#请假领域
├── entity------------------------------#实体/值对象  
│   ├── approval_info.go----------------#审核意见实体
│   ├── leave.go------------------------#请假实体
│   └── valueobject---------------------#值对象
│       ├── applicant.go----------------#申请者值对象
│       ├── approval_type.go------------#审核类型值对象
│       ├── approver.go-----------------#审核人值对象
│       ├── leave_type.go---------------#请假类型值对象
│       └── status.go-------------------#状态值对象

其中,请假实体比较复杂,同时这个请假实体也是聚合根,可以看下请假实体的实现。

package entity

import (
	"learn/ddd/leave/domain/leave/entity/valueobject"
)

// 请假聚合根
type Leave struct {
	id                     string                //请假聚合根ID
	applicant              valueobject.Applicant //申请人值对象
	approver               valueobject.Approver  //审批人值对象
	leaveType              valueobject.LeaveType
	status                 int
	startTime              int
	endTime                int
	currentApproveInfo     ApprovalInfo   //当前审批意见
	historyApproveInfoList []ApprovalInfo //历史审批意见
}

// 创建一个请假单
func (l *Leave) Create(startTime int, endTime int, approver valueobject.Approver) {
	l.startTime = startTime
	l.endTime = endTime
	l.status = valueobject.StatusApproving
}

// 新增历史审批意见
func (l *Leave) AddHistoryApproveInfoList(approvalInfo ApprovalInfo) {
	if l.historyApproveInfoList == nil {
		l.historyApproveInfoList = make([]ApprovalInfo, 0)
	}

	l.historyApproveInfoList = append(l.historyApproveInfoList, approvalInfo)
}

// 通过审核
func (l *Leave) Agree() {
	l.status = valueobject.StatusApproved
}

//驳回审批
func (l *Leave) Reject() {
	l.status = valueobject.StatusReject
}

func (l *Leave) ID() string {
	return l.id
}

//当前审核信息
func (l *Leave) CurrentApproveInfo() ApprovalInfo {
	return l.currentApproveInfo
}

//申请人
func (l *Leave) Applicant() valueobject.Applicant {
	return l.applicant
}

// 获取请假时长
func (l *Leave) Duration() int {
	return l.endTime - l.startTime
}

// 请假类型
func (l *Leave) LeaveType() valueobject.LeaveType {
	return l.leaveType
}

领域服务

领域服务会直接调用聚合根,仓储,事件中心完成整体的领域逻辑,以提交请假单为例子,领域服务代码如下

// 创建请假单
func (l *LeaveDomainService) CreateLeave(leave entity.Leave, leaderMaxLevel int, approver valueobject.Approver) {
	//第一步:完成业务逻辑
	startTime := 127937927423
	endTime := 1232432423423
	leave.Create(startTime, endTime, approver)

	//第二步:完成数据持久化
	l.leaveRepositoryInterface.SaveLeavePo(leave)

	//第三步:发布领域事件
	l.eventPublisher.Publish(leave)
}


 

 

Pre: 2022年书单

Next: DDD系列-战术设计

261
Sign in to leave a comment.
No Leanote account? Sign up now.
0 comments
Table of content