learn.lianglianglee.com/专栏/领域驱动设计实践(完)/085 案例 薪资管理系统的测试驱动开发(上).md.html
2022-05-11 19:04:14 +08:00

1465 lines
47 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>085 案例 薪资管理系统的测试驱动开发(上).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 class="current-tab" 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 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>085 案例 薪资管理系统的测试驱动开发(上)</h1>
<h3>回顾薪资管理系统的设计建模</h3>
<p>在 3-15 课,我们通过场景驱动设计完成了薪资管理系统的领域设计建模。既然场景驱动设计可以很好地与测试驱动开发融合在一起,因此根据场景驱动设计的成果来开展测试驱动开发,就是一个水到渠成的过程。让我们先来看看针对薪资管理系统“支付薪资”领域场景分解的任务:</p>
<ul>
<li>确定是否支付日期
<ul>
<li>确定是否为周五</li>
<li>确定是否为月末工作日
<ul>
<li>获取当月的假期信息</li>
<li>确定当月的最后一个工作日</li>
</ul>
</li>
<li>确定是否为间隔一周周五
<ul>
<li>获取上一次销售人员的支付日期</li>
<li>确定是否间隔了一周</li>
</ul>
</li>
</ul>
</li>
<li>计算雇员薪资
<ul>
<li>计算钟点工薪资
<ul>
<li>获取钟点工雇员与工作时间卡</li>
<li>根据雇员日薪计算薪资</li>
</ul>
</li>
<li>计算月薪雇员薪资
<ul>
<li>获取月薪雇员与考勤记录</li>
<li>对月薪雇员计算月薪</li>
</ul>
</li>
<li>计算销售人员薪资
<ul>
<li>获取销售雇员与销售凭条</li>
<li>根据酬金规则计算薪资</li>
</ul>
</li>
</ul>
</li>
<li>支付
<ul>
<li>向满足条件的雇员账户发起转账</li>
<li>生成支付凭条</li>
</ul>
</li>
</ul>
<p>根据任务分解驱动出来的时序图完整脚本则如下所示:</p>
<pre><code>PaymentAppService.pay() {
PaymentService.pay() {
PayDayService.isPayday(today) {
Calendar.isFriday(today);
WorkdayService.isLastWorkday(today) {
HolidayRepository.ofMonth(month);
Calendar.isLastWorkday(holidays);
}
WorkdayService.isIntervalFriday(today) {
PaymentRepository.lastPayday(today);
Calendar.isFriday(today);
}
}
PayrollCalculator.calculate(employees) {
HourlyEmployeePayrollCalculator.calculate() {
HourlyEmployeeRepository.all();
while (employee -&gt; List&lt;HourlyEmployee&gt;) {
employee.payroll(PayPeriod);
}
}
SalariedEmployeePayrollCalculator.calculate() {
SalariedEmployeeRepository.all();
while (employee -&gt; List&lt;SalariedEmployee&gt;) {
employee.payroll();
}
}
CommissionedEmployeePayrollCalculator.calculate() {
CommissionedEmployeeRepository.all();
while (employee -&gt; List&lt;CommissionedEmployee&gt;) {
employee.payroll(payPeriod);
}
}
}
PayingPayrollService.execute(employees) {
TransferClient.transfer(account);
PaymentRepository.add(payment);
}
}
}
</code></pre>
<h3>测试驱动的方向</h3>
<p>有了分解的任务,也有了履行职责的各个角色构造型,现在是万事俱备只欠东风。让我们严格按照测试驱动开发的红绿黄节奏以及三定律开展领域实现建模。首先,我们要选择需要添加测试的新功能。场景驱动设计在分解任务时,是从外部代表业务价值的领域场景逐步向内推进和拆分的,这是一个从外向内的驱动设计方向;测试驱动开发则不同,为了尽可能避免编写需要模拟的单元测试,应该从内部代表业务实现的原子任务开始,先完成细粒度的自给自足的领域行为逻辑单元,然后逐步往外推进,直到完成满足完整领域场景的所有任务,这是一个从内向外的驱动开发方向:</p>
<p><img src="assets/7f16ed50-0482-11ea-94ff-3fdf236404a5" alt="71557437.png" /></p>
<p>这就意味着在开始测试驱动开发之前,我们需要选择合适的任务。需要考虑的因素包括:</p>
<ul>
<li>任务的依赖性</li>
<li>任务的重要性</li>
</ul>
<p>从依赖的角度看,并不一定需要优先选择前序任务,因为我们可以使用模拟的方式驱动出当前任务需要依赖的接口,而无需考虑实现。不过,基于场景驱动开发分解的任务层次,为其编写测试用例时,也应优先挑选无需访问外部资源的原子任务,即为聚合编写单元测试,因为它无需任何模拟行为。至于任务的重要性,主要是判断任务是否整个系统或模块的核心功能。在确定了领域场景的前提下,一个判断标准是确定任务是主要流程还是异常流程。通常而言,应优先考虑任务的主流程。</p>
<p>显然,支付薪资领域场景的核心功能是支付与薪资计算。由于支付由外部服务完成,剩下要实现的核心功能就是薪资计算。如果从原子任务开始挑选,应首先从内部的原子任务开始挑选,例如选择“根据雇员日薪计算薪资”原子任务:</p>
<ul>
<li>计算雇员薪资
<ul>
<li>计算钟点工薪资
<ul>
<li>获取钟点工雇员与工作时间卡</li>
<li><strong>根据雇员日薪计算薪资</strong></li>
</ul>
</li>
<li>计算月薪雇员薪资
<ul>
<li>获取月薪雇员与考勤记录</li>
<li>对月薪雇员计算月薪</li>
</ul>
</li>
<li>计算销售人员薪资
<ul>
<li>获取销售雇员与销售凭条</li>
<li>根据酬金规则计算薪资</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3>测试驱动开发的过程</h3>
<h4>编写失败的测试</h4>
<p>现在需要为该子任务编写测试用例。根据钟点工薪资的计算规则,可以分为两个不同的测试用例:正常工作时长和加班工作时长。由于场景驱动设计已经确定了履行该原子任务职责的是 HourlyEmployee遵循测试驱动开发的定律一“一次只写一个刚好失败的测试作为新加功能的描述”编写一个刚好失败的测试</p>
<pre><code class="language-java">public class HourlyEmployeeTest {
@Test
public void should_calculate_payroll_by_work_hours_in_a_week() {
}
}
</code></pre>
<p>按照 Given-When-Then 模式来编写该测试方法。首先考虑 HourlyEmployee 聚合的创建。由于钟点工每天都要提交工作时间卡,薪资按周结算,因此在创建 HourlyEmployee 聚合根的实例时,需要传入工作时间卡的列表。计算薪资的方法为 payroll(),返回结果为薪资模型对象 Payroll。验证时需确保薪资的结算周期与薪资总额是正确的。故而编写的测试方法为</p>
<pre><code class="language-java"> @Test
public void should_calculate_payroll_by_work_hours_in_a_week() {
//given
TimeCard timeCard1 = new TimeCard(LocalDate.of(2019, 9, 2), 8);
TimeCard timeCard2 = new TimeCard(LocalDate.of(2019, 9, 3), 8);
TimeCard timeCard3 = new TimeCard(LocalDate.of(2019, 9, 4), 8);
TimeCard timeCard4 = new TimeCard(LocalDate.of(2019, 9, 5), 8);
TimeCard timeCard5 = new TimeCard(LocalDate.of(2019, 9, 6), 8);
List&lt;TimeCard&gt; timeCards = new ArrayList&lt;&gt;();
timeCards.add(timeCard1);
timeCards.add(timeCard2);
timeCards.add(timeCard3);
timeCards.add(timeCard4);
timeCards.add(timeCard5);
HourlyEmployee hourlyEmployee = new HourlyEmployee(timeCards, Money.of(10000, Currency.RMB));
//when
Payroll payroll = hourlyEmployee.payroll();
//then
assertThat(payroll).isNotNull();
assertThat(payroll.beginDate()).isEqualTo(LocalDate.of(2019, 9, 2));
assertThat(payroll.endDate()).isEqualTo(LocalDate.of(2019, 9, 6));
assertThat(payroll.amount()).isEqualTo(Money.of(400000, Currency.RMB));
}
</code></pre>
<p>运行测试,失败:</p>
<p><img src="assets/9974ce10-0482-11ea-883c-cdadeedbf7d8" alt="56636479.png" /></p>
<h4>让失败的测试刚好通过</h4>
<p>在实现测试时,遵循测试驱动开发定律二“不写任何产品代码,除非它刚好能让失败的测试通过”,在实现 payroll() 方法时,仅提供满足当前测试用例预期的实现。什么是“刚好能让失败的测试通过”?以当前测试方法为例。要计算钟点工的薪资,除了它提供的工作时间卡之外,还需要钟点工的时薪,至于 HourlyEmployee 的其他属性,暂时可不用考虑;当前测试方法没有要求验证工作时间卡的有效性,在实现时,亦不必验证传入的工作时间卡是否符合要求,只需确保为测试方法准备的数据是正确的即可;当前测试方法是针对正常工作时长计算薪资,实现时就无需考虑加班的情况。实现代码为:</p>
<pre><code class="language-java">public class HourlyEmployee {
private List&lt;TimeCard&gt; timeCards;
private Money salaryOfHour;
public HourlyEmployee(List&lt;TimeCard&gt; timeCards, Money salaryOfHour) {
this.timeCards = timeCards;
this.salaryOfHour = salaryOfHour;
}
public Payroll payroll() {
int totalHours = timeCards.stream()
.map(tc -&gt; tc.workHours())
.reduce(0, (hours, total) -&gt; hours + total);
Collections.sort(timeCards);
return new Payroll(timeCards.get(0).workDay(), timeCards.get(timeCards.size() - 1).workDay(), salaryOfHour.multiply(totalHours));
}
}
</code></pre>
<p>在编写让失败测试通过的代码时,要把握好分寸,既不要过度地实现测试没有覆盖的内容,也无需死板地拘泥于编写所谓“简单”的实现代码。<strong>简单并非简陋</strong>,既然你的编码技能与设计水平已经足以一次编写出优良的代码,就不必一定要拖到最后,多此一举地等待重构来改进。例如,在上述实现代码中,需要将工作总小时数乘以 Money 类型的时薪,你当然可以实现为如下代码:</p>
<pre><code class="language-java">new Money(salaryOfHour.value() * totalHours, salaryOfHour.currency())
</code></pre>
<p>然而,如果你已经熟悉迪米特法则,且认识到以数据提供者形式进行对象协作的弊病,就会自然地想到应该在 Money 中定义 multiply() 方法,而非通过公开 value 和 currency 的 get 访问器让调用者完成乘法计算。这时就可直接实现如下代码,而不必等着以后再来进行重构:</p>
<pre><code class="language-java">public class Money {
private final long value;
private final Currency currency;
public static Money of(long value, Currency currency) {
return new Money(value, currency);
}
private Money(long value, Currency currency) {
this.value = value;
this.currency = currency;
}
public Money multiply(int factor) {
return new Money(value * factor, currency);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return value == money.value &amp;&amp;
currency == money.currency;
}
@Override
public int hashCode() {
return Objects.hash(value, currency);
}
}
</code></pre>
<p>简单说来,在不会导致过度设计的前提下,若能直接编写出整洁的代码,又何乐而不为呢?只需要做到实现的代码仅仅能让测试刚好通过,不去过度设计即可。为了让测试方法通过,我们定义并实现了 HourlyEmployee、TimeCard 与 Payroll 等领域模型对象。它们的定义都非常简单,即使你知道 HourlyEmployee 一定还有 Id 和 name 等基本的核心字段,也不必在现在就给出这些字段的定义。利用测试驱动开发来实现领域模型,重要的一点就是<strong>要用测试来驱动出这些模型对象的定义</strong>。只要不会遗漏领域场景,就一定会有测试去覆盖这些领域逻辑。一次只做好一件事情即可。</p>
<p>现在测试变绿了:</p>
<p><img src="assets/cc2b70c0-0482-11ea-94ff-3fdf236404a5" alt="82470824.png" /></p>
<p>在测试通过的情况下,先不要考虑是重构还是编写新的测试,而应提交代码。持续集成强调七步提交法,其基础就是进行频繁的原子提交。这样就能保证尽快将你的最新变更反馈到团队共享的代码库上,降低代码冲突的风险,同时也能为重构设定一个安全的回滚版本。</p>
<h4>重构产品代码和测试代码</h4>
<p>提交代码后,根据简单设计原则,我们需要检查已有实现与测试代码是否存在重复,是否清晰地表达了设计者意图。</p>
<p>先看产品代码,目前的实现并没有重复代码,但是 payroll() 方法中的代码 <code>Collections.sort(timeCards);</code> 会让人产生困惑:为什么需要对工作时间卡排序?显然,这里缺乏对业务含义的封装,直接将实现暴露出来了。排序仅仅是手段,我们的目标是获得结算薪资的开始日期和结束日期。由于返回的是两个值,且这两个值代表了一个内聚的概念,故而可以定义一个内部概念 Peroid。重构的过程是首先提取 beginDate 和 endDate 变量,然后定义 Period 内部类:</p>
<pre><code class="language-java"> public Payroll payroll() {
int totalHours = timeCards.stream()
.map(tc -&gt; tc.workHours())
.reduce(0, (hours, total) -&gt; hours + total);
Collections.sort(timeCards);
LocalDate beginDate = timeCards.get(0).workDay();
LocalDate endDate = timeCards.get(timeCards.size() - 1).workDay();
Period settlementPeriod = new Period(beginDate, endDate);
return new Payroll(settlementPeriod.beginDate, settlementPeriod.endDate, salaryOfHour.multiply(totalHours));
}
private class Period {
private LocalDate beginDate;
private LocalDate endDate;
Period(LocalDate beginDate, LocalDate endDate) {
this.beginDate = beginDate;
this.endDate = endDate;
}
}
</code></pre>
<p>然后,再提取方法 settlementPeriod()。该方法名直接体现其业务目标,并将包括排序在内的实现细节封装起来:</p>
<pre><code class="language-java"> public Payroll payroll() {
int totalHours = timeCards.stream()
.map(tc -&gt; tc.workHours())
.reduce(0, (hours, total) -&gt; hours + total);
return new Payroll(
settlementPeriod().beginDate,
settlementPeriod().endDate,
salaryOfHour.multiply(totalHours));
}
private Period settlementPeriod() {
Collections.sort(timeCards);
LocalDate beginDate = timeCards.get(0).workDay();
LocalDate endDate = timeCards.get(timeCards.size() - 1).workDay();
return new Period(beginDate, endDate);
}
</code></pre>
<p>接下来,不要忘了对测试代码的重构。毫无疑问,创建 List<TimeCard> 的逻辑可以封装为一个方法,不至于让测试的 Given 部分充斥太多不必要的细节:</p>
<pre><code class="language-java"> public class HourlyEmployeeTest {
@Test
public void should_calculate_payroll_by_work_hours_in_a_week() {
//given
List&lt;TimeCard&gt; timeCards = createTimeCards();
Money salaryOfHour = Money.of(10000, Currency.RMB);
HourlyEmployee hourlyEmployee = new HourlyEmployee(timeCards, salaryOfHour);
//when
Payroll payroll = hourlyEmployee.payroll();
//then
assertThat(payroll).isNotNull();
assertThat(payroll.beginDate()).isEqualTo(LocalDate.of(2019, 9, 2));
assertThat(payroll.endDate()).isEqualTo(LocalDate.of(2019, 9, 6));
assertThat(payroll.amount()).isEqualTo(Money.of(400000, Currency.RMB));
}
private List&lt;TimeCard&gt; createTimeCards() {
TimeCard timeCard1 = new TimeCard(LocalDate.of(2019, 9, 2), 8);
TimeCard timeCard2 = new TimeCard(LocalDate.of(2019, 9, 3), 8);
TimeCard timeCard3 = new TimeCard(LocalDate.of(2019, 9, 4), 8);
TimeCard timeCard4 = new TimeCard(LocalDate.of(2019, 9, 5), 8);
TimeCard timeCard5 = new TimeCard(LocalDate.of(2019, 9, 6), 8);
List&lt;TimeCard&gt; timeCards = new ArrayList&lt;&gt;();
timeCards.add(timeCard1);
timeCards.add(timeCard2);
timeCards.add(timeCard3);
timeCards.add(timeCard4);
timeCards.add(timeCard5);
return timeCards;
}
}
</code></pre>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/领域驱动设计实践(完)/084 深入理解简单设计.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/领域驱动设计实践(完)/086 案例 薪资管理系统的测试驱动开发(下).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":"70997ec2ec6d3cfa","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>