learn.lianglianglee.com/专栏/领域驱动设计实践(完)/102 实践 考勤上下文的领域建模.md.html
2022-05-11 19:04:14 +08:00

1363 lines
48 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>102 实践 考勤上下文的领域建模.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/领域驱动设计实践(完)/001 「战略篇」访谈 DDD 和微服务是什么关系?.md.html">001 「战略篇」访谈 DDD 和微服务是什么关系?.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/002 「战略篇」开篇词:领域驱动设计,重焕青春的设计经典.md.html">002 「战略篇」开篇词:领域驱动设计,重焕青春的设计经典.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/003 领域驱动设计概览.md.html">003 领域驱动设计概览.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/004 深入分析软件的复杂度.md.html">004 深入分析软件的复杂度.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/005 控制软件复杂度的原则.md.html">005 控制软件复杂度的原则.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/006 领域驱动设计对软件复杂度的应对(上).md.html">006 领域驱动设计对软件复杂度的应对(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/007 领域驱动设计对软件复杂度的应对(下).md.html">007 领域驱动设计对软件复杂度的应对(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/008 软件开发团队的沟通与协作.md.html">008 软件开发团队的沟通与协作.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/009 运用领域场景分析提炼领域知识(上).md.html">009 运用领域场景分析提炼领域知识(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/010 运用领域场景分析提炼领域知识(下).md.html">010 运用领域场景分析提炼领域知识(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/011 建立统一语言.md.html">011 建立统一语言.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/012 理解限界上下文.md.html">012 理解限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/013 限界上下文的控制力(上).md.html">013 限界上下文的控制力(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/014 限界上下文的控制力(下).md.html">014 限界上下文的控制力(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/015 识别限界上下文(上).md.html">015 识别限界上下文(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/016 识别限界上下文(下).md.html">016 识别限界上下文(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/017 理解上下文映射.md.html">017 理解上下文映射.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/018 上下文映射的团队协作模式.md.html">018 上下文映射的团队协作模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/019 上下文映射的通信集成模式.md.html">019 上下文映射的通信集成模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/020 辨别限界上下文的协作关系(上).md.html">020 辨别限界上下文的协作关系(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/021 辨别限界上下文的协作关系(下).md.html">021 辨别限界上下文的协作关系(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/022 认识分层架构.md.html">022 认识分层架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/023 分层架构的演化.md.html">023 分层架构的演化.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/024 领域驱动架构的演进.md.html">024 领域驱动架构的演进.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/025 案例 层次的职责与协作关系(图文篇).md.html">025 案例 层次的职责与协作关系(图文篇).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/026 限界上下文与架构.md.html">026 限界上下文与架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/027 限界上下文对架构的影响.md.html">027 限界上下文对架构的影响.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/028 领域驱动设计的代码模型.md.html">028 领域驱动设计的代码模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/029 代码模型的架构决策.md.html">029 代码模型的架构决策.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/030 实践 先启阶段的需求分析.md.html">030 实践 先启阶段的需求分析.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/031 实践 先启阶段的领域场景分析(上).md.html">031 实践 先启阶段的领域场景分析(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/032 实践 先启阶段的领域场景分析(下).md.html">032 实践 先启阶段的领域场景分析(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/033 实践 识别限界上下文.md.html">033 实践 识别限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/034 实践 确定限界上下文的协作关系.md.html">034 实践 确定限界上下文的协作关系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/035 实践 EAS 的整体架构.md.html">035 实践 EAS 的整体架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/036 「战术篇」访谈DDD 能帮开发团队提高设计水平吗?.md.html">036 「战术篇」访谈DDD 能帮开发团队提高设计水平吗?.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/037 「战术篇」开篇词:领域驱动设计的不确定性.md.html">037 「战术篇」开篇词:领域驱动设计的不确定性.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/038 什么是模型.md.html">038 什么是模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/039 数据分析模型.md.html">039 数据分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/040 数据设计模型.md.html">040 数据设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/041 数据模型与对象模型.md.html">041 数据模型与对象模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/042 数据实现模型.md.html">042 数据实现模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/043 案例 培训管理系统.md.html">043 案例 培训管理系统.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/044 服务资源模型.md.html">044 服务资源模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/045 服务行为模型.md.html">045 服务行为模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/046 服务设计模型.md.html">046 服务设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/047 领域模型驱动设计.md.html">047 领域模型驱动设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/048 领域实现模型.md.html">048 领域实现模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/049 理解领域模型.md.html">049 理解领域模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/050 领域模型与结构范式.md.html">050 领域模型与结构范式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/051 领域模型与对象范式(上).md.html">051 领域模型与对象范式(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/052 领域模型与对象范式(中).md.html">052 领域模型与对象范式(中).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/053 领域模型与对象范式(下).md.html">053 领域模型与对象范式(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/054 领域模型与函数范式.md.html">054 领域模型与函数范式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/055 领域驱动分层架构与对象模型.md.html">055 领域驱动分层架构与对象模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/056 统一语言与领域分析模型.md.html">056 统一语言与领域分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/057 精炼领域分析模型.md.html">057 精炼领域分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/058 彩色 UML 与彩色建模.md.html">058 彩色 UML 与彩色建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/059 四色建模法.md.html">059 四色建模法.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/060 案例 订单核心流程的四色建模.md.html">060 案例 订单核心流程的四色建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/061 事件风暴与业务全景探索.md.html">061 事件风暴与业务全景探索.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/062 事件风暴与领域分析建模.md.html">062 事件风暴与领域分析建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/063 案例 订单核心流程的事件风暴.md.html">063 案例 订单核心流程的事件风暴.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/064 表达领域设计模型.md.html">064 表达领域设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/065 实体.md.html">065 实体.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/066 值对象.md.html">066 值对象.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/067 对象图与聚合.md.html">067 对象图与聚合.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/068 聚合设计原则.md.html">068 聚合设计原则.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/069 聚合之间的关系.md.html">069 聚合之间的关系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/070 聚合的设计过程.md.html">070 聚合的设计过程.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/071 案例 培训领域模型的聚合设计.md.html">071 案例 培训领域模型的聚合设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/072 领域模型对象的生命周期-工厂.md.html">072 领域模型对象的生命周期-工厂.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/073 领域模型对象的生命周期-资源库.md.html">073 领域模型对象的生命周期-资源库.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/074 领域服务.md.html">074 领域服务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/075 案例 领域设计模型的价值.md.html">075 案例 领域设计模型的价值.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/076 应用服务.md.html">076 应用服务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/077 场景的设计驱动力.md.html">077 场景的设计驱动力.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/078 案例 薪资管理系统的场景驱动设计.md.html">078 案例 薪资管理系统的场景驱动设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/079 场景驱动设计与 DCI 模式.md.html">079 场景驱动设计与 DCI 模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/080 领域事件.md.html">080 领域事件.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/081 发布者—订阅者模式.md.html">081 发布者—订阅者模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/082 事件溯源模式.md.html">082 事件溯源模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/083 测试优先的领域实现建模.md.html">083 测试优先的领域实现建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/084 深入理解简单设计.md.html">084 深入理解简单设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/085 案例 薪资管理系统的测试驱动开发(上).md.html">085 案例 薪资管理系统的测试驱动开发(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/086 案例 薪资管理系统的测试驱动开发(下).md.html">086 案例 薪资管理系统的测试驱动开发(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/087 对象关系映射(上).md.html">087 对象关系映射(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/088 对象关系映射(下).md.html">088 对象关系映射(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/089 领域模型与数据模型.md.html">089 领域模型与数据模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/090 领域驱动设计对持久化的影响.md.html">090 领域驱动设计对持久化的影响.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/091 领域驱动设计体系.md.html">091 领域驱动设计体系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/092 子领域与限界上下文.md.html">092 子领域与限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/093 限界上下文的边界与协作.md.html">093 限界上下文的边界与协作.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/094 限界上下文之间的分布式通信.md.html">094 限界上下文之间的分布式通信.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/095 命令查询职责分离.md.html">095 命令查询职责分离.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/096 分布式柔性事务.md.html">096 分布式柔性事务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/097 设计概念的统一语言.md.html">097 设计概念的统一语言.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/098 模型对象.md.html">098 模型对象.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/099 领域驱动设计参考过程模型.md.html">099 领域驱动设计参考过程模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/100 领域驱动设计的精髓.md.html">100 领域驱动设计的精髓.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/101 实践 员工上下文的领域建模.md.html">101 实践 员工上下文的领域建模.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/领域驱动设计实践(完)/102 实践 考勤上下文的领域建模.md.html">102 实践 考勤上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/103 实践 项目上下文的领域建模.md.html">103 实践 项目上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/104 实践 培训上下文的业务需求.md.html">104 实践 培训上下文的业务需求.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/105 实践 培训上下文的领域分析建模.md.html">105 实践 培训上下文的领域分析建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/106 实践 培训上下文的领域设计建模.md.html">106 实践 培训上下文的领域设计建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/107 实践 培训上下文的领域实现建模.md.html">107 实践 培训上下文的领域实现建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/108 实践 EAS 系统的代码模型.md.html">108 实践 EAS 系统的代码模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/109 后记:如何学习领域驱动设计.md.html">109 后记:如何学习领域驱动设计.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>102 实践 考勤上下文的领域建模</h1>
<h3>考勤上下文的领域建模</h3>
<h4><strong>业务需求</strong></h4>
<p>考勤上下文主要包括员工考勤管理、请假和假期管理等功能。</p>
<p>员工上下班时,需要打卡进行考勤。员工打卡的操作通过考勤机完成,因此,该用例实际上并不在 EAS 系统范围之内。EAS 系统会定期访问考勤机的接口导入打卡记录,导入成功后,员工可以查询自己的打卡记录和出勤记录。注意打卡记录和出勤记录之间的差异。打卡记录是考勤机每天留存的信息,出勤记录则是根据集团的考勤制度并结合员工的请假信息和打卡记录生成的记录内容。因此,出勤记录会根据考勤制度确定员工的出勤状态。</p>
<p>员工通过系统提交请假申请,包括请假的日期、天数和请假类型。只有在该员工主管审批通过请假申请后,请假才会生效。请假与考勤息息相关,在确定员工的出勤情况时,需要根据请假情况进行判断。</p>
<p>中国的国家法定节假日设定没有固定的规律,不管是传统节日还是元旦、国庆或五一之类的假期,每年都在发生变化,需要遵循国家假日办每年给出的节假日安排进行设定。同时,工作日的工作时间规则也支持可配置。上班时间与下班时间直接影响员工的出勤状态。目前,公司并未实行弹性工作制。</p>
<h4><strong>领域分析模型</strong></h4>
<p>考勤上下文的领域逻辑并不复杂,通过名词动词法即可从业务需求的描述中发现主要的领域概念。在确定领域分析模型时,需要确定统一语言,例如在需求分析中,已经阐述了打卡记录与出勤记录之间的差异,前者为 TimeCard后者为 Attendance。员工每打一次卡就会产生一条打卡记录正常情况下每位员工一天会形成两条打卡记录分别记载了上班时间和下班时间并由此确定员工的出勤状态。为了区分原始的打卡记录与处理后的打卡记录经过咨询领域专家为其确定了各自的统一语言。前者命名为 PunchedCard如此可以更好地形容打卡的动作后者命名为 TimeCard用以记录员工的工作时间。</p>
<p>员工请假时,从业务需求描述看来是一个请假申请,但事实上就是一个 Leave 领域概念审批请假申请意味着修改它的状态ApprovedStatus。请假时员工应指定请假的类型LeaveType</p>
<p>由于法定节假日可能出现因为周末与节日的重叠而导致的调休情况为了简化领域模型可以将全年的每一天分为假日Holiday和工作日Workday。在设置节假日时只需设置具体的假日即可故而只需定义 Holiday 领域类它指定了节假日包括周末的期限。同理工作时间规则WorktimeRule也可由管理人员进行配置。由此可以得出如下领域分析模型</p>
<p><img src="assets/9032f9a0-3225-11ea-9c3e-cbf2baa3b166" alt="39084286.png" /></p>
<p>这个领域分析模型虽然简单,却具有一定的代表性。主要体现在:</p>
<ul>
<li>Holiday 与 Worktime 是两个完全独立的领域对象,与别的领域对象没有任何关联。在确定领域分析模型时,不要因为出现了这样“孤单”的领域对象,就认为出现了建模错误,非要给它寻找一个关联对象。</li>
<li>寻找精准简洁的统一语言表达领域概念,例如 Holiday 体现了节假日的设置,没有必要将其命名为 HolidaySettingAttendance 是一次出勤记录,在定义类名时,也不必为其加上后缀 Record。这样的修饰纯属画蛇添足影响了领域概念的清晰表达最终还会影响领域实现模型。</li>
<li>TimeCard、PunchedCard 与 Attendance 都依赖了员工上下文的 Employee。由于领域模型需要界定限界上下文在分析模型中可以先用不同颜色表示它属于另外一个限界上下文到领域设计建模时就可以提醒设计者考虑直接引用 EmployeeId还是引入对应的 Employee 领域类。</li>
</ul>
<h4><strong>领域设计模型</strong></h4>
<p>领域分析模型已经清晰地展现了领域对象之间的关系,因此在设计建模时,只需确定实体和值对象,界定聚合的边界就能水到渠成。但是,这里需要考虑 TimeCard 的生命周期,它究竟是在处理打卡记录 PunchedCard 之后生成的瞬时对象,还是需要进行持久化,以便于系统对其进行生命周期的管理?由于出勤记录 Attendance 已经记录了员工上下班时间和出勤状态,弱化了 TimeCard 的上下班时间的管理功能,无需考虑它的生命周期,故而定义为瞬时对象(图中可标记为白色)。</p>
<p>Holiday 与 WorktimeRule 的设计亦较特殊。本质上它们应属于值对象可通过值进行判等。例如Holiday 对象通过 year、date 与 holidayType 的值即可确定 Holiday 的唯一性。然而,按照领域驱动设计的设计纪律,资源库只能管理聚合的生命周期,而聚合的根又只能是一个实体,因此需要将它们从值对象升级为实体。至于 PunchedCard、Attendance 等聚合根实体与 Employee 之间的关系只能通过 EmployeeId 进行关联,由此可以得到如下领域设计模型:</p>
<p><img src="assets/d710c280-3225-11ea-a4f2-37e03842b3a7" alt="39121611.png" /></p>
<p>同样采用场景驱动设计识别领域场景,并获得该场景对应的时序图。以“生成出勤记录”领域场景为例,分解的任务如下所示:</p>
<ul>
<li>生成出勤记录
<ul>
<li>获取员工信息</li>
<li>生成员工出勤记录
<ul>
<li>获取员工工作时间
<ul>
<li>根据日期获取员工的打卡记录</li>
<li>生成员工工作时间</li>
</ul>
</li>
<li>获取工作时间规则</li>
<li>确定是否节假日</li>
<li>获取员工请假信息</li>
<li>确定出勤状态</li>
</ul>
</li>
<li>保存出勤记录</li>
</ul>
</li>
</ul>
<p>将领域场景分配给 AttendanceAppService组合任务“生成出勤记录”分配给领域服务 AttendancesGenerator然后将访问数据库的原子任务分配给相应聚合的资源库然后将获得的信息作为方法参数传给聚合 Attendance由其确定出勤状态。在为每位员工创建了具有正确出勤状态的 Attendance 对象后,由该聚合的资源库 AttendanceRepository 实现所有出勤记录的持久化。整个协作过程如下图所示:</p>
<p><img src="assets/952d8b80-36d7-11ea-a3f2-df8dff95a3b3" alt="33367611.png" /></p>
<p>该时序图脚本如下所示:</p>
<pre><code>AttendanceAppService.generate(day) {
AttendancesGenerator.generate(day) {
List&lt;String&gt; eployeeIds = EmployeeClient.allEmployeeIds();
for (String empId : employeeIds) {
AttendanceGenerator.generate(empId, day) {
TimeCardGenerator.generate(empId, day) {
PunchedCardRepository.punchedCardsOf(empId, day);
TimeCard.createFrom(List&lt;PunchedCard&gt;);
}
WorktimeRule worktimeRule = WorktimeRuleRepository.worktimeRule();
boolean isHoliday = HolidayRepository.isHoliday(day);
Leave leave = LeaveRepository.leaveOf(empId, day);
Attendance.assureStatus(timeCard, worktimeRule, leave, isHoliday);
}
}
AttendanceRepository.addAll(attendances);
}
}
</code></pre>
<p>AttendanceGenerator 领域服务承担了大量的协作工作,该领域场景的主要领域逻辑则封装在 Attendance 聚合根实体,由它接受工作时间记录、工作时间规则、请假信息与假期信息,确定该员工的出勤状况。显然,该时序图可以作为领域设计模型的一部分,更好地为领域实现模型提供指导。</p>
<h4><strong>领域实现模型</strong></h4>
<p>针对“生成出勤记录”领域场景,与领域相关的内部原子任务为“确定出勤状态”,它持有该领域场景的核心领域逻辑。为其编写测试用例,包括:</p>
<ul>
<li>日期为假期,员工无考勤记录或为无效考勤记录,出勤状态为 Holiday</li>
<li>日期为假期,员工有有效考勤记录,出勤状态为 Overtime</li>
<li>日期为工作日,员工有考勤记录,上下班打卡时间满足工作时间规则定义,出勤状态为 Normal</li>
<li>日期为工作日,员工有考勤记录,上班打卡事件晚于上班时间定义范围,出勤状态为 Late</li>
<li>日期为工作日,员工有考勤记录,下班打卡事件早于上班时间定义范围,出勤状态为 LeaveEarly</li>
<li>日期为工作日,员工有考勤记录,上班打卡事件晚于上班时间定义范围,下班打卡事件早于上班时间定义范围,出勤状态为 LateAndLeaveEarly</li>
<li>日期为工作日,员工无考勤记录,无请假记录,出勤状态为 Absence</li>
<li>日期为工作日,员工无考勤记录,有请假记录,出勤状态为请假类型</li>
</ul>
<p>针对这些测试用例,一一编写测试方法,最后得到的测试类如下代码所示:</p>
<pre><code class="language-java">public class AttendanceTest {
@Test
public void should_be_HOLIDAY_on_holiday_without_time_card() {
// given
Attendance attendance = new Attendance(employeeId, workDay);
// when
attendance.assureStatus(beHoliday, null, null);
// then
assertThat(attendance.status()).isEqualTo(AttendanceStatus.Holiday);
}
@Test
public void should_be_HOLIDAY_on_holiday_with_invalid_time_card() {
// given
LocalTime punchedStartWork = LocalTime.of(9, 00);
LocalTime punchedEndWork = LocalTime.of(12, 59);
TimeCard timeCard = TimeCard.of(workDay, punchedStartWork, punchedEndWork, workTimeRule);
Attendance attendance = new Attendance(employeeId, workDay);
// when
attendance.assureStatus(beHoliday, timeCard, null);
// then
assertThat(attendance.status()).isEqualTo(AttendanceStatus.Holiday);
}
@Test
public void should_be_OVERTIME_on_holiday_with_valid_time_card() {
// given
TimeCard timeCard = TimeCard.of(workDay, startWork, endWork, workTimeRule);
Attendance attendance = new Attendance(employeeId, workDay);
// when
attendance.assureStatus(beHoliday, timeCard, null);
// then
assertThat(attendance.status()).isEqualTo(AttendanceStatus.Overtime);
}
@Test
public void should_be_NORMAL_on_workday_with_time_card() {
// given
TimeCard timeCard = TimeCard.of(workDay, startWork, endWork, workTimeRule);
Attendance attendance = new Attendance(employeeId, workDay);
// when
attendance.assureStatus(notHoliday, timeCard, null);
// then
assertThat(attendance.status()).isEqualTo(AttendanceStatus.Normal);
}
@Test
public void should_be_LATE_on_workday_with_time_card_and_be_late_to_start_work() {
// given
LocalTime punchedStartWork = LocalTime.of(9, 16);
TimeCard timeCard = TimeCard.of(workDay, punchedStartWork, endWork, workTimeRule);
Attendance attendance = new Attendance(employeeId, workDay);
// when
attendance.assureStatus(notHoliday, timeCard, null);
// then
assertThat(attendance.status()).isEqualTo(AttendanceStatus.Late);
}
@Test
public void should_be_LEAVE_EARLY_on_workday_with_time_card_and_be_earlier_than_end_work() {
// given
LocalTime punchedEndWork = LocalTime.of(5, 44);
TimeCard timeCard = TimeCard.of(workDay, startWork, punchedEndWork, workTimeRule);
Attendance attendance = new Attendance(employeeId, workDay);
// when
attendance.assureStatus(notHoliday, timeCard, null);
// then
assertThat(attendance.status()).isEqualTo(AttendanceStatus.LeaveEarly);
}
@Test
public void should_be_LATE_AND_LEAVE_EARLY_on_workday_with_time_card_and_be_late_to_start_work_and_earlier_than_end_work() {
// given
LocalTime punchedStartWork = LocalTime.of(9, 16);
LocalTime punchedEndWork = LocalTime.of(5, 44);
TimeCard timeCard = TimeCard.of(workDay, punchedStartWork, punchedEndWork, workTimeRule);
Attendance attendance = new Attendance(employeeId, workDay);
// when
attendance.assureStatus(notHoliday, timeCard, null);
// then
assertThat(attendance.status()).isEqualTo(AttendanceStatus.LateAndLeaveEarly);
}
@Test
public void should_be_LEAVE_on_workday_without_time_card_and_with_leave() {
// given
LocalDate askLeaveDay = LocalDate.of(2019, 12, 22);
Leave leave = Leave.of(employeeId, askLeaveDay, LeaveType.Sick);
Attendance attendance = new Attendance(employeeId, workDay);
// when
attendance.assureStatus(notHoliday, null, leave);
// then
assertThat(attendance.status()).isEqualTo(AttendanceStatus.SickLeave);
}
@Test
public void should_be_ABSENCE_on_workday_without_time_card_and_leave() {
// given
Attendance attendance = new Attendance(employeeId, workDay);
// when
attendance.assureStatus(notHoliday, null, null);
// then
assertThat(attendance.status()).isEqualTo(AttendanceStatus.Absence);
}
}
</code></pre>
<p>遵循测试驱动开发的过程,并严格按照简单设计原则获得的产品代码如下所示:</p>
<pre><code class="language-java">package xyz.zhangyi.ddd.eas.attendancecontext.domain;
public class Attendance {
private AttendanceStatus status;
private String employeeId;
private LocalDate workDay;
public Attendance(String employeeId, LocalDate workDay) {
this.employeeId = employeeId;
this.workDay = workDay;
}
public void assureStatus(boolean isHoliday, TimeCard timeCard, Leave leave) {
status = withCondition(isHoliday, timeCard, leave).toStatus();
}
private Condition withCondition(boolean isHoliday, TimeCard timeCard, Leave leave) {
if (timeCard != null &amp;&amp; !timeCard.sameWorkDay(workDay)) {
throw new InvalidAttendanceException(&quot;different work day for attendance, time card and leave&quot;);
}
if (leave != null &amp;&amp; !leave.sameDay(workDay)) {
throw new InvalidAttendanceException(&quot;different work day for attendance, time card and leave&quot;);
}
return new Condition(isHoliday, timeCard, leave);
}
public AttendanceStatus status() {
return status;
}
}
</code></pre>
<p>代码中的 Condition 类是通过重构提取的参数对象Parameter Object它封装了具体的条件判断逻辑。Attendance 类的现有实现是通过测试驱动和重构一步一步改进获得的,具体的测试驱动开发过程与重构过程可以通过 GitHub 获取 <a href="https://github.com/agiledon/eas-ddd">EAS Repository</a> 的提交历史一窥究竟。</p>
<p>对比领域实现模型的产品代码与领域设计模型的时序图脚本,或许能发现一些细微的差异。最初通过场景驱动设计分解任务时,在为 Attendance 确定出勤状态时,需要传入 WorkTimeRule 对象,以便于获知公司规定的上下班时间。但在实现时,我发现该信息交由 TimeCard 持有会更加合理,可由其确定员工是否迟到、早退,因而调整了实现:</p>
<pre><code class="language-java">public class TimeCard {
private LocalDate workDay;
private LocalTime startWork;
private LocalTime endWork;
private WorkTimeRule workTimeRule;
private TimeCard(LocalDate workDay,
LocalTime startWork,
LocalTime endWork,
WorkTimeRule workTimeRule) {
this.workDay = workDay;
this.startWork = startWork;
this.endWork = endWork;
this.workTimeRule = workTimeRule;
}
public static TimeCard of(LocalDate workDay,
LocalTime startWork,
LocalTime endWork,
WorkTimeRule workTimeRule) {
return new TimeCard(workDay, startWork, endWork, workTimeRule);
}
public boolean isLate() {
return workTimeRule.isLate(startWork);
}
public boolean isLeaveEarly() {
return workTimeRule.isLeaveEarly(endWork);
}
}
</code></pre>
<p>在实现这些领域类的领域行为时,需要时刻把握正确的对象协作方式,并遵循“信息专家”模式来分配职责。如此一来,就能保证每个对象都能各司其职,让职责的分配更合理,也能避免贫血模型,定义不必要的 getter 与 setter 访问器。当然,我们也要随时注意领域模型在不同阶段存在的差异,必须做好模型的同步。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/领域驱动设计实践(完)/101 实践 员工上下文的领域建模.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/领域驱动设计实践(完)/103 实践 项目上下文的领域建模.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997eeeab7b3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>