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

1337 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>103 实践 项目上下文的领域建模.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 class="current-tab" 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>103 实践 项目上下文的领域建模</h1>
<h3>项目上下文的领域建模</h3>
<h4><strong>业务需求</strong></h4>
<p>项目上下文的业务需求主要包括:项目管理、项目成员管理、迭代与问题管理等功能。</p>
<p>管理员可以创建项目指定项目类型并配置项目的基本信息。项目管理人员可以为项目添加项目成员并指定项目成员的角色。当一名员工成为该项目的成员后参与该项目的经验就可以作为简历的一部分。项目管理人员可以创建一个或多个迭代并为每个迭代确定一个迭代目标。如果没有创建迭代系统会为项目创建一个默认的待办项Backlog迭代。创建迭代时需要指定迭代的周期即开始时间和截止时间。迭代需要手动选择开始才会生效。迭代开始时需要根据当前日期显示剩余天数如果当前时间到达截止日期迭代也不会自动关闭只会提示“剩余 0 天”,项目管理人员可以手动地关闭迭代,并将当前迭代未完成的问题移到下一个已经开始或还未开始的迭代。如果没有符合条件的迭代,这些问题会移到待办项迭代中。</p>
<p>一个问题Issue可以是软件的缺陷一个项目的具体任务一个业务功能需求或者是一个需要解决的技术难题等。创建问题时需要指定问题所属的项目、问题类型、问题概要和报告人设置问题的优先级以及问题的经办人。如果未指定经办人系统会自动将创建问题的用户设置为经办人。当然还需要输入问题的描述并设置该问题的标签。关注该问题的用户可以为问题添加评论或者上传附件。在创建问题时还可以指定问题所属的迭代指定的迭代只能是当前正在进行或未来要进行的迭代。</p>
<p>每个问题有一个状态,用来表明问题所处的阶段。这些状态包括:</p>
<ul>
<li>Open表示问题被提交等待团队成员处理。</li>
<li>In Progress问题在处理中尚未完成。</li>
<li>Resolved问题已解决但解决结论需要确认。</li>
<li>Reopened已解决的问题未获认可。</li>
<li>Closed已解决的问题得到认可和确认置为关闭状态。</li>
</ul>
<p>问题的默认状态为 Open。如果该问题已经分配给项目成员并开始解决该问题时项目成员需要手动将问题状态修改为 In Progress。一旦解决了该问题就应标记该问题为 Resolved。只有问题的报告人才可以修改问题的状态为 Reopened 或 Closed。任何状态的变更都会发送邮件通知经办人和报告人。由于每个问题可以创建多个子任务当问题位于处理中状态时若其下的子任务还未标记为 Resolved在标记问题为 Resolved 时,需提示用户:部分子任务还未解决。若用户确认该问题已经解决,并将其标记为 Resolved 状态时,该问题下的所有子任务状态也必须标记为 Resolved。如果为问题创建了子任务则关注该问题的用户也可以为子任务添加评论。</p>
<p>在创建或编辑问题时团队成员可以对问题进行评估给出问题的故事点Story Point。在对迭代和项目进行汇总统计时可以根据问题或故事点进行汇总统计。</p>
<p>问题被创建后团队成员可以编辑问题例如修改标题、描述、估算、重新分配报告人和经办人等。每次对问题的修改都需要记录下来作为当前问题的变更记录ChangeHistory。如果修改了报告人需要向之前的报告人和目前负责的报告人发送邮件通知如果修改了经办人需要向报告人以及之前的经办人、目前负责的经办人发送邮件通知。当一个问题分配给团队成员时团队成员可以在该问题下填写项目日志。</p>
<h3>领域分析模型</h3>
<p>分析项目上下文的需求,可以通过名词动词法初步获得项目上下文的主要领域概念,包括:</p>
<ul>
<li>项目Project</li>
<li>项目成员TeamMember</li>
<li>迭代Iteration</li>
<li>问题Issue</li>
<li>子任务SubTask</li>
<li>评论Comment</li>
<li>附件Attachment</li>
<li>变更记录ChangeHistory</li>
</ul>
<p>项目、迭代、问题与子任务存在非常清晰的一对多组合关系,它们也构成了项目上下文的“骨架”。项目成员作为参与项目管理活动的角色,是主要业务用例的参与者。每个问题可以有多个附件,问题与子任务还可以有多个评论。每次对问题的修改与变更,都会生成一条变更记录。于是,可以快速获得如下的领域分析模型:</p>
<p><img src="assets/5dc5ee80-3614-11ea-b651-9bf55e9590d3" alt="50958028.png" /></p>
<p>领域分析模型除了包含主要的领域概念之外,还将一些主要的属性定义为领域类,同时确定了它们之间的关系。由于一个问题只能指定一个报告人和一个经手人,因此 Issue 与 TeamMember 之间的关系是一对二的关系。</p>
<h3>领域设计模型</h3>
<p>要获得项目上下文的领域设计模型,仍然可以采用庖丁解牛的过程进行模型的细化。</p>
<p>首先理顺对象图明确各个类之间的关系。项目上下文各个类之间的关系非常清晰很容易辨别类之间的面向对象合成或聚合关系。区分合成和聚合只需判断主类是否必须拥有从类的属性值。例如Issue 必须指定 IssueType 和 IssueStatus因此是合成关系但它未必需要划分 SubType也未必一定拥有 Comment 与 Attachment它们之间的关系就是聚合关系。由于需求要求一个项目必须定义至少一个迭代如果没有手动创建迭代系统会默认创建待办项迭代因此 Project 与 Iteration 之间的关系是合成关系。</p>
<p>确定实体还是值对象也非常容易。由于业务需求的主要领域概念都需要身份标识来辨别其身份,故而定义为实体。至于这些实体的属性多数被定义为值对象,因为它们代表的领域概念只需要关心其值,无需身份标识,如 StoryPoint、IssueType 等类。由此可获得梳理后的领域对象图:</p>
<p><img src="assets/fb8fcd60-3615-11ea-b5d4-6937111bfc43" alt="48637403.png" /></p>
<p>既然这些类之间的关系要么是合成关系要么是聚合关系通过分解关系薄弱处来划定聚合边界也变得非常容易。但是需要注意两点特殊之处。其一Project 和 Issue 与 TeamMember 之间都存在合成关系,且 TeamMember 是一个实体由于实体不能被两个聚合同时调用因此只能将这三个实体定义为三个独立的聚合。其二Issue 与 StoryPoint 之间的关系虽然是面向对象的聚合关系,按照依赖强弱,可以考虑将 StoryPoint 与 Issue 分开,但由于 StoryPoint 是值对象,不能独立定义为一个聚合,只能划到 Issue 实体的边界内:</p>
<p><img src="assets/0e848e10-3616-11ea-996b-ef6591d33435" alt="48727830.png" /></p>
<p>确定了初步的聚合边界之后我们需要遵循聚合设计的原则来调整已有边界。Project 与 Iteration 虽然是依赖较强的合成关系,一个项目也确实需要至少一个迭代存在,但由于 Iteration 允许调用者能够直接操作和管理迭代的生命周期,具有独立性,故而需要单独为迭代划定聚合边界。</p>
<p>Issue 与 SubTask 本身是面向对象的聚合关系,一个问题也可以没有子任务;然而,一旦问题划分了子任务,问题的状态就要受到子任务状态的约束。例如,在将问题的状态设置为 Resolved 时,必须检查该问题下所有子任务的状态是否已被设置为 Resolved子任务的状态必须与问题的状态保持一致这实际上是 Issue 与 SubTask 之间存在的不变量Invariant</p>
<p>Issue 与 SubTask 之间的不变量带来了聚合设计的一个分歧。若依据不变量原则,这两个实体应放在同一个聚合中。但是,问题与子任务又都可以添加评论,由于 Comment 是一个单独的聚合,若要表示子任务与评论之间的关系,又该如何表达呢?毕竟,此时的 SubTask 只是 Issue 聚合内部的实体,它的 ID 不能暴露给当前聚合外的其他聚合。这就是聚合设计的为难之处。我们能选择的方案有以下三种:</p>
<ul>
<li>A 方案:为 Issue、SubTask 和 Comment 建立三个单独的聚合,即意味着牺牲不变量</li>
<li>B 方案:将 Issue、SubTask 和 Comment 放在一个聚合中,即意味着聚合粒度变大</li>
<li>C 方案:将 Issue 与 SubTask 放在一个聚合中,然后暴露各自的 Id 给 Comment 聚合,即意味着破坏了聚合边界</li>
</ul>
<p>我们需要评估哪一个方案带来的优势更大哪一个方案带来的问题更少。A 方案将 SubTask 放在 Issue 聚合之外,意味着调用者可以通过 SubTask 的资源库单独管理子任务的生命周期,在没有 Issue 边界的控制下,很难保证 Issue 与 SubTask 之间状态的一致。B 方案会形成一个粒度较大的聚合,且 Comment 的管理只能通过 Issue 聚合根实体进行无法单独管理存在不便。C 方案满足了 Issue 与 SubTask 的不变量,也满足了 Comment 的独立性,但在一定程度上破坏了聚合边界的封装性。</p>
<p>每个方案都有自己的问题相比较而言C 方案带来的优势更大。在无法改变业务需求的情况下,我更倾向于这一方案:</p>
<p><img src="assets/38e87400-3616-11ea-a962-5985f456c479" alt="50868838.png" /></p>
<p>聚合根实体分别为Project、Iteration、Issue、TeamMember、Comment、Attachment 与 ChangeHistory。图中仍然用面向对象的合成或聚合表现聚合根之间的关系但在设计时上游聚合根应通过 ID 与下游聚合根建立关联关系。比较特殊的是 Issue 聚合根,它需要提供 IssueId 与 SubTaskId 和下游聚合 Comment 建立关联。</p>
<p>通过用例可以确定业务场景,并利用场景驱动设计细化领域设计模型。例如“分配问题给项目成员”领域场景,可以分解任务为:</p>
<ul>
<li>分配问题给项目成员
<ul>
<li>获得问题</li>
<li>分配问题给经办人</li>
<li>更新问题</li>
<li>创建问题的变更记录</li>
<li>通知报告人
<ul>
<li>生成报告人通知</li>
<li>发送通知</li>
</ul>
</li>
<li>通知经办人
<ul>
<li>获取经办人信息</li>
<li>生成经办人通知</li>
<li>发送通知</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>根据角色构造型进行职责分配获得时序图如下所示:</p>
<p><img src="assets/5dae0ed0-3616-11ea-996b-ef6591d33435" alt="58998764.png" /></p>
<p>这一领域场景看似简单,但它实际上牵涉到项目上下文多个领域对象,以及与 OA 集成上下文、员工上下文之间的协作。其中EmployeeClient 与 NotificationClient 是针对员工上下文与 OA 集成上下文定义的南向网关(属于防腐层)。该场景的时序图脚本如下所示:</p>
<pre><code>IssueAppService.assign(issueId, owner) {
IssueService.assign(issueId, owner) {
Issue issue = IssueRepository.issueOf(issueId);
issue.assignTo(ownerId);
IssueRepository.update(issue);
ChangeHistoryRepository.add(changeHistory);
NotificationService.notifyOwner(issue, owner) {
AssignmentNotification notification = AssignmentNotification.forOwner(issue, owner);
NotificationClient.notify(NotificationRequest.from(notification));
}
NotificationService.notifyReporter(issue) {
EmployeeResponse empResponse = EmployeeClient.employeeOf(reporterId);
AssignmentNotification notification = AssignmentNotification.forReporter(issue, empResponse.toReporter());
NotificationClient.notify(NotificationRequest.from(notification));
}
}
}
</code></pre>
<p>应用服务 IssueAppService 对外提供支持该领域场景的服务方法,在其内部,又将该职责委派给了 IssueService 领域服务,由它负责协调 IssueRepository 资源库与 Issue 聚合根实体。一旦问题分配完毕IssueService 领域服务会调用 ChangeHistoryRepository 资源库创建一条新的变更记录,然后通过 NotificationService 领域服务发送通知。承担问题的项目成员就是问题的经办人OwnerAssignmentNotification 领域对象可以通过 Issue 与提供了经办人信息的 TeamMember 生成通知内容,交由 NotificationClient 网关发送通知。由于 Issue 仅持有报告人的 employeeId即图中的 reporterId),因此还需要通过 EmployeeClient 网关获得报告人reporter的详细信息再由 AssignmentNotification 领域对象生成通知内容,最后发送通知给该问题的报告人。</p>
<p>AssignmentNotification 是领域分析建模以及识别聚合的领域设计建模阶段未能识别出来的领域对象。它属于项目上下文持有了与问题分配相关的领域知识。NotificationClient 网关接口定义的 NotificationRequest 实际上是 AssignmentNotification 对应的消息契约对象,它能够将 AssignmentNotification 转换为通知上下文需要的请求对象。AssignmentNotification 领域对象虽然封装了领域信息和领域逻辑,但它并不需要持久化,故而属于一个瞬时对象。</p>
<h3>领域实现模型</h3>
<p>采用场景驱动设计对领域进行设计建模后,根据领域场景继续领域实现建模可谓水到渠成。仍然从领域场景拆分的任务开始编写测试用例,然后采用测试驱动开发一步步驱动出领域逻辑的实现代码。仍以“分配问题给项目成员”领域场景为例,选择位于该场景内部的原子任务“分配问题给经办人”,确定如下测试用例:</p>
<ul>
<li>问题被分配给指定经办人,并创建变更记录</li>
<li>状态为 Resolved 或 Closed 的问题不可再分配给经办人</li>
<li>问题不可分配给相同的经办人</li>
</ul>
<p>对应的测试方法为:</p>
<pre><code class="language-java">public class IssueTest {
@Test
public void should_assign_to_specific_owner_and_generate_change_history() {
Issue issue = Issue.of(issueId, name, description);
ChangeHistory history = issue.assignTo(owner, operator);
assertThat(issue.ownerId()).isEqualTo(owner.id());
assertThat(history.issueId()).isEqualTo(issueId.id());
assertThat(history.operatedBy()).isEqualTo(operator);
assertThat(history.operation()).isEqualTo(Operation.Assignment);
assertThat(history.operatedAt()).isEqualToIgnoringSeconds(LocalDateTime.now());
}
@Test
public void should_throw_AssignmentIssueException_when_assign_resolved_issue() {
Issue issue = Issue.of(issueId, name, description);
issue.changeStatusTo(IssueStatus.Resolved);
assertThatThrownBy(() -&gt; issue.assignTo(owner, operator))
.isInstanceOf(AssignmentIssueException.class)
.hasMessageContaining(&quot;resolved issue can not be assigned&quot;);
}
@Test
public void should_throw_AssignmentIssueException_when_assign_closed_issue() {
Issue issue = Issue.of(issueId, name, description);
issue.changeStatusTo(IssueStatus.Closed);
assertThatThrownBy(() -&gt; issue.assignTo(owner, operator))
.isInstanceOf(AssignmentIssueException.class)
.hasMessageContaining(&quot;closed issue can not be assigned&quot;);
}
@Test
public void should_throw_AssignmentIssueException_when_issue_is_assigned_to_same_owner() {
Issue issue = Issue.of(issueId, name, description);
issue.assignTo(owner, operator);
assertThatThrownBy(() -&gt; issue.assignTo(owner, operator))
.isInstanceOf(AssignmentIssueException.class)
.hasMessageContaining(&quot;issue can not be assign to same owner again&quot;);
}
}
</code></pre>
<p>聚合根实体 Issue 负责完成对问题的分配,方法 assignTo() 的实现为:</p>
<pre><code class="language-java">public class Issue {
public ChangeHistory assignTo(IssueOwner owner, Operator operator) {
if (status.isResolved()) {
throw new AssignmentIssueException(&quot;resolved issue can not be assigned.&quot;);
}
if (status.isClosed()) {
throw new AssignmentIssueException(&quot;closed issue can not be assigned.&quot;);
}
if (this.ownerId != null &amp;&amp; this.ownerId.equals(owner.id())) {
throw new AssignmentIssueException(&quot;issue can not be assign to same owner again.&quot;);
}
this.ownerId = owner.id();
return ChangeHistory
.operate(Operation.Assignment)
.to(issueId.id())
.by(operator)
.at(LocalDateTime.now());
}
}
</code></pre>
<p>由于问题的每次变更都需要记录变更记录,且变更记录还需要保留操作者的信息,故而需要在 assignTo() 方法中添加操作人员的信息。Issue 与 ChangeHistory 分属两个聚合。由于 Issue 持有了创建变更记录的信息,因此由它作为 ChangeHisitory 聚合的工厂;同时,变更记录作为 assignTo() 方法的返回结果,也符合领域逻辑的要求。</p>
<p>领域服务 IssueService 负责协调 Issue 聚合与资源库,由其完成相对完整的问题分配功能。在为其编写测试用例时,勿需重复编写业已覆盖的测试用例。例如,需要考虑根据问题 ID 未找到问题的测试用例,但勿需再考虑分配的问题状态不合理的测试用例,因为后者在为 Issue 编写测试时已经覆盖了。换言之,在为 IssueService 编写测试时,应该认为 <code>issue.assignTo(owner, operator)</code> 方法的实现是正确的。IssueService 的测试方法为:</p>
<pre><code class="language-java">public class IssueServiceTest {
@Test
public void should_assign_issue_to_specific_owner_and_generate_change_history() {
Issue issue = Issue.of(issueId, &quot;test issue&quot;, &quot;test desc&quot;);
IssueRepository issueRepo = mock(IssueRepository.class);
when(issueRepo.issueOf(issueId)).thenReturn(Optional.of(issue));
issueService.setIssueRepository(issueRepo);
ChangeHistoryRepository changeHistoryRepo = mock(ChangeHistoryRepository.class);
issueService.setChangeHistoryRepository(changeHistoryRepo);
issueService.assign(issueId, owner, operator);
assertThat(issue.ownerId()).isEqualTo(owner.id());
verify(issueRepo).issueOf(issueId);
verify(issueRepo).update(issue);
verify(changeHistoryRepo).add(isA(ChangeHistory.class));
}
@Test
public void should_throw_IssueException_given_no_issue_found_given_issueId() {
IssueRepository issueRepo = mock(IssueRepository.class);
when(issueRepo.issueOf(issueId)).thenReturn(Optional.empty());
issueService.setIssueRepository(issueRepo);
ChangeHistoryRepository changeHistoryRepo = mock(ChangeHistoryRepository.class);
issueService.setChangeHistoryRepository(changeHistoryRepo);
assertThatThrownBy(() -&gt; issueService.assign(issueId, owner, operator))
.isInstanceOf(IssueException.class)
.hasMessageContaining(&quot;issue&quot;)
.hasMessageContaining(&quot;not found&quot;);
verify(issueRepo).issueOf(issueId);
verify(issueRepo, never()).update(isA(Issue.class));
verify(changeHistoryRepo, never()).add(isA(ChangeHistory.class));
}
}
</code></pre>
<p>它的实现为:</p>
<pre><code class="language-java">public class IssueService {
private IssueRepository issueRepo;
private ChangeHistoryRepository changeHistoryRepo;
public void assign(IssueId issueId, IssueOwner owner, Operator operator) {
Optional&lt;Issue&gt; optIssue = issueRepo.issueOf(issueId);
Issue issue = optIssue.orElseThrow(() -&gt; issueNotFoundError(issueId));
ChangeHistory changeHistory = issue.assignTo(owner, operator);
issueRepo.update(issue);
changeHistoryRepo.add(changeHistory);
}
private IssueException issueNotFoundError(IssueId issueId) {
return new IssueException(String.format(&quot;issue with id {%s} was not found&quot;, issueId.id()));
}
}
</code></pre>
<p>与 Issue 聚合根实体的测试不同在为领域服务编写测试时只要它依赖了访问外部资源的网关就需要对其进行模拟Mock以保证单元测试的快速反馈。</p>
<p>到目前为止,我们仅仅针对员工上下文、考勤上下文与项目上下文的领域层编写测试与产品代码。即使通过领域服务驱动出了资源库,也仅仅是抽象的接口定义。在合适的时机,我们当然需要实现基础设施的内容,但这种通过领域逻辑驱动出领域实现模型的方式,毫无疑问才是走在领域驱动设计的正确道路上。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/领域驱动设计实践(完)/102 实践 考勤上下文的领域建模.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/领域驱动设计实践(完)/104 实践 培训上下文的业务需求.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":"70997ef119013cfa","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>