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

1460 lines
52 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>107 实践 培训上下文的领域实现建模.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 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 class="current-tab" 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>107 实践 培训上下文的领域实现建模</h1>
<p>假定整个培训上下文通过领域设计建模获得了以聚合为自治单元的领域设计模型识别了每一个领域场景需求分析人员为每个领域场景都编写了具有验收标准Acceptance Criteria的用户故事然后通过场景驱动设计给出了分解好的任务分配了职责的时序图或时序图脚本则开发人员在领域实现建模阶段要做的工作就是针对每个领域场景的任务编写测试用例然后进行测试驱动开发。</p>
<p>在编写领域实现代码时,不能死板地照搬领域设计建模的成果。为了快速地推进领域建模,在进行领域设计建模时,总有可能存在考虑不周之处,尤其是通过场景驱动设计获得的时序图脚本,属于伪代码的编码形式,并未完全真实地呈现最终的实现模型。此外,在领域实现建模阶段,需要以迭代的形式完成任务,加强开发人员与需求分析人员、测试人员的沟通,通过 Kick Off 与 Desk Check 等交流手段统一对需求的认识,就每个用户故事的验收标准达成一致,如此才能够保证编写的测试用例满足客户的需求。</p>
<h3>测试驱动开发</h3>
<h4><strong>聚合的测试驱动开发</strong></h4>
<p>从一个领域场景开始,选择一个表达领域概念和领域行为的原子任务,开始为其编写测试用例。以“提名候选人”领域场景为例,首先选择“提名”原子任务,它的测试用例包括:</p>
<ul>
<li>验证票的状态必须为“Available”</li>
<li>提名给候选人后票的状态更改为“WatiForConfirm”</li>
<li>为票生成提名历史记录</li>
</ul>
<p>由于场景驱动设计已经识别出该任务由 Ticket 聚合履行其职责,故而创建 TicketTest 测试类。为第一个测试用例编写测试如下:</p>
<pre><code class="language-java">public class TicketTest {
private String trainingId;
private Candidate candidate;
@Before
public void setUp() {
trainingId = &quot;111011111111&quot;;
candidate = new Candidate(&quot;200901010110&quot;, &quot;Tom&quot;, &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="89fde6e4c9ece8faa7eae6e4">[email&#160;protected]</a>&quot;, trainingId);
}
@Test
public void should_throw_TicketException_given_ticket_is_not_AVAILABLE() {
Ticket ticket = new Ticket(TicketId.next(), trainingId, TicketStatus.WaitForConfirm);
assertThatThrownBy(() -&gt; ticket.nominate(candidate))
.isInstanceOf(TicketException.class)
.hasMessageContaining(&quot;ticket is not available&quot;);
}
}
</code></pre>
<p>遵循简单设计原则与测试驱动设计三大支柱,只需要编写让该测试通过的实现代码即可:</p>
<pre><code class="language-java">package xyz.zhangyi.ddd.eas.trainingcontext.domain.ticket;
import xyz.zhangyi.ddd.eas.trainingcontext.domain.candidate.Candidate;
import xyz.zhangyi.ddd.eas.trainingcontext.domain.exceptions.TicketException;
import xyz.zhangyi.ddd.eas.trainingcontext.domain.tickethistory.TicketHistory;
public class Ticket {
private TicketId ticketId;
private String trainingId;
private TicketStatus ticketStatus;
public Ticket(TicketId ticketId, String trainingId, TicketStatus ticketStatus) {
this.ticketId = ticketId;
this.trainingId = trainingId;
this.ticketStatus = ticketStatus;
}
public TicketHistory nominate(Candidate candidate) {
if (!ticketStatus.isAvailable()) {
throw new TicketException(&quot;ticket is not available, cannot be nominated.&quot;);
}
return null;
}
}
</code></pre>
<p>由于当前测试并没有验证 nominate(candidate) 方法返回的结果,为了让测试快速通过,可以保留返回 null 的简单实现。</p>
<p>接下来为第二个测试用例编写测试方法:</p>
<pre><code class="language-java">public class TicketTest {
@Test
public void ticket_status_should_be_WAIT_FOR_CONFIRM_after_ticket_was_nominated() {
Ticket ticket = new Ticket(TicketId.next(), trainingId);
ticket.nominate(candidate);
assertThat(ticket.status()).isEqualTo(TicketStatus.WaitForConfirm);
assertThat(ticket.nomineeId()).isEqualTo(candidate.employeeId());
}
}
</code></pre>
<p>该测试仅验证了 ticket 的状态和提名人 ID为了保证测试通过只需做如下实现</p>
<pre><code class="language-java">public class Ticket {
public TicketHistory nominate(Candidate candidate) {
if (!ticketStatus.isAvailable()) {
throw new TicketException(&quot;ticket is not available, cannot be nominated.&quot;);
}
this.ticketStatus = TicketStatus.WaitForConfirm;
this.nomineeId = candidate.employeeId();
return null;
}
}
</code></pre>
<p>在针对第三个测试用例编写测试时,就需要结合业务需求通过验证来驱动出 TicketHistory 类。首先nominate(candidate) 方法返回了 TicketHistory 对象,为了确保返回的结果是正确的,需要验证它的属性值。究竟要验证哪些属性呢?我们可以从测试出发,确定培训票需要保存的历史记录包括:</p>
<ul>
<li>票的 ID</li>
<li>票的操作类型</li>
<li>状态迁移的状况</li>
<li>执行该操作类型后的票的拥有者</li>
<li>谁执行了本次操作</li>
<li>何时执行了本次操作</li>
</ul>
<p>体现为测试方法,即对 ticketHistory 的验证:</p>
<pre><code class="language-java"> @Test
public void should_generate_ticket_history_after_ticket_was_nominated() {
Ticket ticket = new Ticket(TicketId.next(), trainingId);
TicketHistory ticketHistory = ticket.nominate(candidate, nominator);
assertThat(ticketHistory.ticketId()).isEqualTo(ticket.id());
assertThat(ticketHistory.operationType()).isEqualTo(OperationType.Nomination);
assertThat(ticketHistory.owner()).isEqualTo(new TicketOwner(candidate.employeeId(), TicketOwnerType.Nominee));
assertThat(ticketHistory.stateTransit()).isEqualTo(StateTransit.from(TicketStatus.Available).to(TicketStatus.WaitForConfirm));
assertThat(ticketHistory.operatedBy()).isEqualTo(new Operator(nominator.employeeId(), nominator.name()));
assertThat(ticketHistory.operatedAt()).isEqualToIgnoringSeconds(LocalDateTime.now());
}
</code></pre>
<p>在当前领域场景中,票的操作者 operator 就是作为协调者或培训主管的提名人Nominator。由于之前定义的 nominate(candidate) 方法并无提名人的信息,故而需要引入 Nominator 类,修改方法接口为 <code>nominate(candidate, nominator)</code></p>
<p>验证 TicketHistory 的属性值,也驱动出 TicketOwner、StateTransit、OperationType 与 Operator 类,这些类皆作为 TicketHistory 聚合内的值对象,它们在领域设计建模时并没有被识别出。相反,领域设计模型为 TicketHistory 聚合定义了 CancellingReason 与 DeclineReason 类,在当前的 TicketHistory 定义中并没有给出这是因为当前的领域场景还未牵涉到这些领域概念。TicketHistory 类的定义为:</p>
<pre><code class="language-java">public class TicketHistory {
private TicketId ticketId;
private TicketOwner owner;
private StateTransit stateTransit;
private OperationType operationType;
private Operator operatedBy;
private LocalDateTime operatedAt;
public TicketHistory(TicketId ticketId,
TicketOwner owner,
StateTransit stateTransit,
OperationType operationType,
Operator operatedBy,
LocalDateTime operatedAt) {
this.ticketId = ticketId;
this.owner = owner;
this.stateTransit = stateTransit;
this.operationType = operationType;
this.operatedBy = operatedBy;
this.operatedAt = operatedAt;
}
public TicketId ticketId() {
return this.ticketId;
}
public TicketOwner owner() {
return this.owner;
}
public StateTransit stateTransit() {
return this.stateTransit;
}
public OperationType operationType() {
return this.operationType;
}
public Operator operatedBy() {
return this.operatedBy;
}
public LocalDateTime operatedAt() {
return this.operatedAt;
}
}
</code></pre>
<p>为了让当前测试快速通过Ticket 的 <code>nominate(candidate, nominator)</code> 方法实现为:</p>
<pre><code class="language-java"> public TicketHistory nominate(Candidate candidate, Nominator nominator) {
if (!ticketStatus.isAvailable()) {
throw new TicketException(&quot;ticket is not available, cannot be nominated.&quot;);
}
this.ticketStatus = TicketStatus.WaitForConfirm;
this.nomineeId = candidate.employeeId();
return new TicketHistory(ticketId,
new TicketOwner(candidate.employeeId(), TicketOwnerType.Nominee),
StateTransit.from(TicketStatus.Available).to(this.ticketStatus),
OperationType.Nomination,
new Operator(nominator.employeeId(), nominator.name()),
LocalDateTime.now());
}
</code></pre>
<p>考虑到 TicketOwner 的属性值来自 CandidateOperator 的属性值来自 Nominator可以将 Candidate 与 Nominator 分别视为它们的工厂。因而可以重构代码:</p>
<pre><code class="language-java"> public TicketHistory nominate(Candidate candidate, Nominator nominator) {
if (!ticketStatus.isAvailable()) {
throw new TicketException(&quot;ticket is not available, cannot be nominated.&quot;);
}
this.ticketStatus = TicketStatus.WaitForConfirm;
this.nomineeId = candidate.employeeId();
return new TicketHistory(ticketId,
candidate.toOwner(),
transitState(),
OperationType.Nomination,
nominator.toOperator(),
LocalDateTime.now());
}
</code></pre>
<p>通过提取方法,该方法还可以进一步精简为:</p>
<pre><code class="language-java"> public TicketHistory nominate(Candidate candidate, Nominator nominator) {
validateTicketStatus();
doNomination(candidate);
return generateHistory(candidate, nominator);
}
</code></pre>
<p>对比测试用例,你会发现重构后的方法包含的三行代码恰好对应这三个测试用例,清晰地展现了“提名候选人”的执行步骤。</p>
<p>当然,测试代码也可以进一步重构:</p>
<pre><code class="language-java"> @Test
public void should_generate_ticket_history_after_ticket_was_nominated() {
Ticket ticket = new Ticket(TicketId.next(), trainingId);
TicketHistory ticketHistory = ticket.nominate(candidate, nominator);
assertTicketHistory(ticket, ticketHistory);
}
</code></pre>
<h4><strong>领域服务的测试驱动开发</strong></h4>
<p>在为原子任务编写了产品代码和测试代码之后,即可在此基础上开始领域服务的测试驱动开发。领域服务对应一个组合任务,除了访问外部资源的原子任务之外,若其余原子任务都已完成编码实现,就能降低为领域服务编写单元测试的成本。与 TicketService 领域服务对应的组合任务为“提名候选人”。需要考虑的测试用例为:</p>
<ul>
<li>没有符合条件的 Ticket抛出 TicketException</li>
<li>培训票被成功提名给候选人</li>
</ul>
<p>在考虑候选人被提名后的验收标准时,通过开发人员、需求分析人员与测试人员对需求的沟通,发现之前编写的用户故事中,忽略了两个功能:</p>
<ul>
<li>添加票的历史记录</li>
<li>候选人被提名之后的处理,需要将被提名者从该培训的候选人名单中移除</li>
</ul>
<p>故而需要调整该领域服务对应的时序图脚本:</p>
<pre><code>TicketService.nominate(ticketId, candidate, nominator) {
TicketRepository.ticketOf(ticketId);
Ticket.nominate(candidate, nominator);
TicketRepository.update(ticket);
TicketHistoryRepository.add
CandidateRepository.remove(candidate);
}
</code></pre>
<p>现在,针对测试用例编写测试方法:</p>
<pre><code class="language-java">public class TicketServiceTest {
@Test
public void should_throw_TicketException_if_available_ticket_not_found() {
TicketId ticketId = TicketId.next();
TicketRepository mockTickRepo = mock(TicketRepository.class);
when(mockTickRepo.ticketOf(ticketId, Available)).thenReturn(Optional.empty());
TicketService ticketService = new TicketService();
ticketService.setTicketRepository(mockTickRepo);
String trainingId = &quot;111011111111&quot;;
Candidate candidate = new Candidate(&quot;200901010110&quot;, &quot;Tom&quot;, &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="44302b29042125376a272b29">[email&#160;protected]</a>&quot;, trainingId);
Nominator nominator = new Nominator(&quot;200901010007&quot;, &quot;admin&quot;, &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b9d8ddd4d0d7f9dcd8ca97dad6d4">[email&#160;protected]</a>&quot;, TrainingRole.Coordinator);
assertThatThrownBy(() -&gt; ticketService.nominate(ticketId, candidate, nominator))
.isInstanceOf(TicketException.class)
.hasMessageContaining(String.format(&quot;available ticket by id {%s} is not found&quot;, ticketId.id()));
verify(mockTickRepo).ticketOf(ticketId, Available);
}
}
</code></pre>
<p>通过 Mockito的mock() 方法模拟 TicketRepository 获取 Ticket 的行为,并假定返回 <code>Optional.empty()</code>,以模拟未能找到培训票的场景。注意,在验证该方法时,除了要验证指定异常的抛出之外,还需要通过 Mockito 的 verify() 方法验证领域服务与资源库的协作。实现代码为:</p>
<pre><code class="language-java"> public class TicketService {
private TicketRepository tickRepo;
public void setTicketRepository(TicketRepository tickRepo) {
this.tickRepo = tickRepo;
}
public void nominate(TicketId ticketId, Candidate candidate, Nominator nominator) {
Optional&lt;Ticket&gt; optionalTicket = tickRepo.ticketOf(ticketId, TicketStatus.Available);
if (!optionalTicket.isPresent()) {
throw new TicketException(String.format(&quot;available ticket by id {%s} is not found.&quot;, ticketId));
}
}
}
</code></pre>
<p>驱动出来的 TicketRepository 定义为:</p>
<pre><code class="language-java">public interface TicketRepository {
Optional&lt;Ticket&gt; ticketOf(TicketId ticketId, TicketStatus ticketStatus);
}
</code></pre>
<p>为 TicketService 编写的第二个测试需要验证提名候选人的结果。由于原子任务“提名”已经被 Ticket 的测试完全覆盖,故而在领域服务的测试中,只需要验证聚合与资源库之间的协作逻辑即可。如此既能保证代码质量和测试覆盖率,又可减少编写和维护测试的成本:</p>
<pre><code class="language-java"> @Test
public void should_nominate_candidate_for_specific_ticket() {
// given
String trainingId = &quot;111011111111&quot;;
TicketId ticketId = TicketId.next();
Ticket ticket = new Ticket(TicketId.next(), trainingId, Available);
TicketRepository mockTickRepo = mock(TicketRepository.class);
when(mockTickRepo.ticketOf(ticketId, Available)).thenReturn(Optional.of(ticket));
TicketHistoryRepository mockTicketHistoryRepo = mock(TicketHistoryRepository.class);
CandidateRepository mockCandidateRepo = mock(CandidateRepository.class);
TicketService ticketService = new TicketService();
ticketService.setTicketRepository(mockTickRepo);
ticketService.setTicketHistoryRepository(mockTicketHistoryRepo);
ticketService.setCandidateRepository(mockCandidateRepo);
Candidate candidate = new Candidate(&quot;200901010110&quot;, &quot;Tom&quot;, &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f98d9694b99c988ad79a9694">[email&#160;protected]</a>&quot;, trainingId);
Nominator nominator = new Nominator(&quot;200901010007&quot;, &quot;admin&quot;, &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="7c1d181115123c191d0f521f1311">[email&#160;protected]</a>&quot;, TrainingRole.Coordinator);
// when
ticketService.nominate(ticketId, candidate, nominator);
// then
verify(mockTickRepo).ticketOf(ticketId, Available);
verify(mockTickRepo).update(ticket);
verify(mockTicketHistoryRepo).add(isA(TicketHistory.class));
verify(mockCandidateRepo).remove(candidate);
}
</code></pre>
<p>编写以上测试方法,不仅能验证 TicketService 的功能,同时还能驱动出各个资源库的接口。</p>
<p>与该测试对应的实现为:</p>
<pre><code class="language-java">public class TicketService {
private TicketRepository tickRepo;
private TicketHistoryRepository ticketHistoryRepo;
private CandidateRepository candidateRepo;
public void nominate(TicketId ticketId, Candidate candidate, Nominator nominator) {
Optional&lt;Ticket&gt; optionalTicket = tickRepo.ticketOf(ticketId, TicketStatus.Available);
Ticket ticket = optionalTicket.orElseThrow(() -&gt; availableTicketNotFound(ticketId));
TicketHistory ticketHistory = ticket.nominate(candidate, nominator);
tickRepo.update(ticket);
ticketHistoryRepo.add(ticketHistory);
candidateRepo.remove(candidate);
}
private TicketException availableTicketNotFound(TicketId ticketId) {
return new TicketException(String.format(&quot;available ticket by id {%s} is not found.&quot;, ticketId));
}
}
</code></pre>
<p>通过测试驱动开发进行领域实现建模是一个层层递进的过程。从领域场景分解的任务看,是从原子任务递进到组合任务;从领域模型对象的角色构造型来看,则是从聚合递进到领域服务。这样的实现既保证了各个类粒度的合理性,又能保证职责的合理分配,避免了所谓的“贫血模型”与“胀血模型”。测试驱动开发的单元测试又奠定了代码重构的基础。若在未来发生需求变更,需要改进现有设计或修改实现,就能保证开发人员进行安全的重构乃至于重写,确保了设计精进的可能性。</p>
<p><strong>领域驱动设计需要以迭代的方式进行增量开发</strong>,我不建议在未开始领域实现建模之前,花费大量的时间打磨领域设计模型。毕竟,一切未曾落地的设计,都可能是镜花水月。因此,我强调领域驱动设计结合敏捷迭代开发,并在敏捷管理流程的指导下合理安排项目开发。例如,在获得需求后,可以针对已有需求开展领域分析建模和领域设计建模,并在设计建模时只需要识别出领域场景即可。这时获得的领域设计模型包含了领域层最为关键的角色构造型:聚合与资源库。</p>
<p>一旦识别出领域场景,需求分析人员与测试人员就可以结对编写用户故事,并将这些用户故事放入到迭代计划中。开发团队在领取用户故事后,通过与需求分析人员、测试人员的 Kick Off彻底了解其领域需求包括用户故事的验收标准并在确认统一语言之后开始场景驱动设计即分解任务然后根据角色构造型编写时序图脚本。编写的时序图脚本以及对应的时序图可以作为领域设计模型的一部分这个过程实际上是测试驱动开发的预研相当于是在开发人员的心智模型中进行了业务流程与软件设计的演练。待最终确定了时序图脚本完成了场景驱动设计就可以开始编写测试用例进行测试驱动开发了。</p>
<p>就一个用户故事而言,从场景驱动开发到测试驱动开发是一个连续的开发过程;就一个限界上下文而言,从领域分析建模到领域设计建模初期(到识别出领域场景为止),是整个特性团队参与建模的过程;识别出领域场景之后,需求分析与迭代增量开发就成了并行与串行交错的两条线,即需求分析人员在进行迭代 N+1 用户故事的分析与编写的同时开发团队进行迭代N的场景驱动设计和测试驱动开发。</p>
<p>在完成领域实现建模的测试驱动开发之后,针对一个领域场景而言,只有完成了应用层和基础设施层的实现编码,才算真正完成整个用户故事。这就需要定义远程服务和应用服务,并完成基础设施层北向网关与南向网关的实现,即领域驱动设计魔方中,纳米层次技术维度要完成的框架应用开发与基础设施代码。它们的设计与开发并不属于领域实现建模的范畴,而应站在系统架构的角度,在分层架构、上下文映射以及前后端分离的背景之下,定义和实现系统的代码模型。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/领域驱动设计实践(完)/106 实践 培训上下文的领域设计建模.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/领域驱动设计实践(完)/108 实践 EAS 系统的代码模型.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997efc2a5c3cfa","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>