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) }
No Leanote account? Sign up now.