learn.lianglianglee.com/专栏/领域驱动设计实践(完)/010 运用领域场景分析提炼领域知识(下).md.html
2022-05-11 19:04:14 +08:00

1312 lines
51 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>010 运用领域场景分析提炼领域知识(下).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 class="current-tab" 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 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>010 运用领域场景分析提炼领域知识(下)</h1>
<h3>用户故事</h3>
<p>敏捷开发人员对用户故事User Story绝不陌生不过很多人并未想过为何极限编程的创始人 Kent Beck 要用用户故事来代替传统的“需求功能点”。传统的需求分析产生的是冷冰冰的需求文档,它把着重点放在了系统功能的精确描述上,却忽略了整个软件系统最重要的核心——<strong>用户</strong>。一个软件系统,只有用户在使用它的功能时才会真正产生价值。传统的功能描述忽略了在需求场景中用户的参与,因而缺乏了需求描写的“身临其境”。用户故事则站在了用户角度,以“讲故事”的方式来阐述需求;这种所谓“故事”其实就是对领域场景的描述,因而一个典型的用户故事,无论形式如何,实质上都是领域场景 6W 模型的体现。</p>
<p>一种经典的用户故事模板要求以如下格式来描述故事:</p>
<pre><code>As a作为&lt;角色&gt;
I would like我希望&lt;活动&gt;
so that以便于&lt;业务价值&gt;
</code></pre>
<p>格式中的角色、活动与业务价值正好对应了 6W 模型的 Who、What 与 Why。形如这样的模板并非形式主义而是希望通过这种显式的格式来推动需求分析师站在用户角色的角度去挖掘隐藏在故事背后的“<strong>业务价值</strong>”。需求分析师要做一个好的故事讲述者,就需要站在角色的角度不停地针对用户故事去问为什么。</p>
<p>针对如下用户故事:</p>
<pre><code>作为一名用户,
我希望可以提供查询功能,
以便于了解分配给我的任务情况。
</code></pre>
<p>我们可以询问如下问题:</p>
<ul>
<li>到底谁是用户?需要执行这一活动的角色到底是谁?</li>
<li>为什么需要查询功能?</li>
<li>究竟要查询什么样的内容?</li>
<li>为什么需要了解分配给我的任务情况?</li>
</ul>
<p>显然前面给出的用户故事含糊不清,并没有清晰地表达业务目标。<strong>这样的用户故事并不利于我们提炼领域知识</strong>。倘若我们将用户识别为项目成员,则这个角色与项目跟踪管理这个场景才能够互相呼应。从角色入手,就可以更好地理解所谓的“业务价值”到底是什么?——项目成员希望跟踪自己的工作进度。如何跟踪工作进度?那就需要获得目前分配给自己的未完成任务。于是,前面的故事描述就应该修改为:</p>
<pre><code>作为一名项目成员,
我希望获取分配给自己的未完成任务,
以便于跟踪自己的工作进度。
</code></pre>
<p>我以“获取”代替“查询”是不希望在用户故事中主观地认定该功能一定是通过查询获得的。“查询Query”这个词语始终还是过于偏向技术实现除非该用户故事本身就是描述搜索查询的业务。</p>
<p>显然,在这个用户故事中,“项目成员”是行为的发起者,“跟踪工作进度”是故事发生的“因”,是行为发起者真正关心的<strong>价值</strong>,为了获得这一价值,所以才“希望获取分配给自己的未完成任务”,是故事发生的果。通过这种深度挖掘价值,就可以帮助我们发现真正的业务功能。业务功能不是“需要提供查询功能”,而是希望系统提供“获取未完成任务”的方法。至于如何获取,则是技术实现层面的细节。</p>
<p>Dean Leffingwell 在《敏捷软件需求》一书中对这三部分做出了如下阐释:</p>
<blockquote>
<p><strong>角色</strong>支持对产品功能的细分,而且它经常引出其他角色的需要以及相关活动的环境;<strong>活动</strong>通常表述相关角色所需的“系统需求”;<strong>价值</strong>则传达为什么要进行相关活动,也经常可以引领团队寻找能够提供相同价值而且更少工作量的替代活动。</p>
</blockquote>
<p>敏捷实践要求需求分析人员与测试人员结对编写用户故事一个完整的用户故事必须是可测试Testable因此验收标准Acceptance Criteria是用户故事不可缺少的部分。所谓“验收标准”是针对系统设立的一些满足条件因此这些标准并非测试的用例而是对业务活动的细节描述有时候甚至建议采用 Given-When-Then 模式结合场景来阐述验收标准,又或者通过实例化需求的方式,直接提供“身临其境”的案例。例如,针对电商的订单处理,需要为订单设置配送免费的总额阈值,用户故事可以编写为:</p>
<pre><code>作为一名销售经理
我希望为订单设置合适的配送免费的总额阈值
以便于促进平均订单总额的提高
验收标准:
* 订单总额的货币单位应以当前国家的货币为准
* 订单总额阈值必须大于0
</code></pre>
<p>如果采用 Given-When-Then 模式,并通过实例化需求的方式编写用户故事,可以改写为:</p>
<pre><code>作为一名销售经理
我希望为订单设置合适的配送免费的总额阈值
以便于促进平均订单总额的提高
场景1订单满足配送免费的总额阈值
Given配送免费的总额阈值设置为95元人民币
And我目前的购物车总计90元人民币
When我将一个价格为5元人民币的商品添加到购物车
Then我将获得配送免费的优惠
场景2订单不满足配送免费的总额阈值
Given配送免费的总额阈值设置为95元人民币
And我目前的购物车总计85元人民币
When我将一个价格为9元人民币的商品添加到购物篮
Then我应该被告知如果我多消费1元人民币就能享受配送免费的优惠
</code></pre>
<p>第一个例子的验收标准更加简洁适合于业务逻辑不是特别复杂的用户故事Given-When-Then 模式的验收标准更加详细和全面,从业务流程的角度去描述,体现了 6W 模型的 hoW但有时候显得过于冗余编写的时间成本更大这两种形式可以根据具体业务酌情选用。</p>
<p>编写用户故事时可以参考行为驱动开发Behavior-Driven DevelopmentBDD的实践即强调<strong>使用 DSLDomain Specific Language领域特定语言描述用户行为编写用户故事</strong>。DSL 是一种编码实现相比自然语言更加精确又能以符合领域概念的形式满足所谓“活文档Living Document”的要求。</p>
<p>行为驱动开发的核心在于“<strong>行为</strong>”。当业务需求被划分为不同的业务场景并以“Given-When-Then”的形式描述出来时就形成了一种范式化的领域建模规约。使用领域特定语言编写用户故事的过程就是不断发现领域概念的过程。这些领域概念会因为在团队形成共识而成为统一语言。这种浮现领域模型与统一语言的过程又反过来可以规范我们对用户故事的编写即按照行为驱动开发的要求将核心放在“领域行为”上。这就需要避免两种错误的倾向</p>
<ul>
<li>从 UI 操作去表现业务行为</li>
<li>描述技术实现而非业务需求</li>
</ul>
<p>例如,我们要编写“发送邮件”这个业务场景的用户故事,可能会写成这样:</p>
<pre><code>Scenario: send email
Given a user &quot;James&quot; with password &quot;123456&quot;
And I sign in
And I fill in &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="abc6c2c0ceebcfcfcfdbd9cac8dfc2c8ce85c8c4c6">[email&#160;protected]</a>&quot; in &quot;to&quot; textbox
And fill in &quot;test email&quot; in &quot;subject&quot; textbox
And fill in &quot;This is a test email&quot; in &quot;body&quot; textarea
When I click the &quot;send email&quot; button
Then the email should be sent sucessfully
And shown with message &quot;the email is sent sucessfully&quot;
</code></pre>
<p>该用户故事描写的不是业务行为,而是用户通过 UI 进行交互的操作流程,这种方式实则是让用户界面捆绑了你对领域行为的认知。准确地说,这种 UI 交互操作并非业务行为,例如上述场景中提到的 button 与 textbox 控件,与发送邮件的功能并没有关系。如果换一个 UI 设计,使用的控件就完全不同了。</p>
<p>那么换成这样的写法呢?</p>
<pre><code>Scenario: send email
Given a user &quot;James&quot; with password &quot;123456&quot;
And I sign in after OAuth authentification
And I fill in &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a3cecac8c6e3c7c7c7d3d1c2c0d7cac0c68dc0ccce">[email&#160;protected]</a>&quot; as receiver
And &quot;test email&quot; as subject
And &quot;This is a test email&quot; as email body
When I send the email
Then it should connect smtp server
And all messages should be composed to email
And a composed email should be sent to receiver via smtp protocal
</code></pre>
<p>该用户故事的编写暴露了不必要的技术细节,如连接到 smtp 服务器、消息组合为邮件、邮件通过 smtp 协议发送等。<strong>我们在编写用户故事时应该按照行为驱动开发的要求关注于做什么what而不是怎么做how</strong>。如果在业务分析过程中,纠缠于技术细节,就可能导致我们忽略了业务价值。<strong>在业务建模阶段,业务才是重心,不能舍本逐末。</strong></p>
<p>那么,该怎么写?</p>
<p>编写用户故事时,不要考虑任何 UI 操作,甚至应该抛开已设计好的 UI 原型,也不要考虑任何技术细节,不要让这些内容来干扰你对业务需求的理解。如果因为更换 UI 设计和调整 UI 布局,又或者因为改变技术实现方案,而需要修改编写好的用户故事,那就是不合理的。<strong>用户故事应该只受到业务规则与业务流程变化的影响。</strong></p>
<p>让我们修改前面的用户故事,改为专注领域行为的形式编写:</p>
<pre><code>Scenario: send email
Given a user &quot;James&quot; with password &quot;123456&quot;
And I sign in
And I fill in a subject with &quot;test email&quot;
And a body with &quot;This is a test email&quot;
When I send the email to &quot;Mike&quot; with address &quot;<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="315c585a547155555541435052455852541f525e5c">[email&#160;protected]</a>&quot;
Then the email should be sent sucessfully
</code></pre>
<p>只要发送邮件的流程与规则不变,这个用户故事就不需要修改。</p>
<h3>测试驱动开发</h3>
<p>测试驱动开发看起来与提炼领域知识风马牛不相及,那是因为我们将测试驱动开发固化为了一种开发实践。测试驱动开发强调“测试优先”,但实质上这种“测试优先”其实是<strong>需求分析优先</strong>,是<strong>任务分解优先</strong>。测试驱动开发强调,开发人员在分析了需求之后,并不是一开始就编写测试,而是必须完成<strong>任务分解</strong>。对任务的分解其实就是对职责的识别,且识别出来的职责在被分解为单独的任务时,必须是可验证的。</p>
<p>在进行测试驱动开发时,虽然要求从一开始就进行任务分解,但并不苛求任务分解是完全合理的。随着测试的推进,倘若我们觉察到一个任务有太多测试用例需要编写,则意味着分解的任务粒度过粗,应对其进行再次分解;也有可能会发现一些我们之前未曾发现的任务,则需要将它们添加到任务列表中。</p>
<p>例如,我们要实现一个猜数字的游戏。游戏有四个格子,每个格子有 0~9 的数字,任意两个格子的数字都不一样。玩家有 6 次猜测的机会,如果猜对则获胜,失败则进入下一轮直到六轮猜测全部结束。每次猜测时,玩家需依序输入 4 个数字程序会根据猜测的情况给出形如“xAxB”的反馈。A 前面的数字代表位置和数字都对的个数B 前面的数字代表数字对但位置不对的个数。例如,答案是 1 2 3 4那么对于不同的输入会有如下的输出</p>
<table>
<thead>
<tr>
<th align="left">输入</th>
<th align="left">输出</th>
<th align="left">说明</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">1 5 6 7</td>
<td align="left">1A0B</td>
<td align="left">1 位置正确</td>
</tr>
<tr>
<td align="left">2 4 7 8</td>
<td align="left">0A2B</td>
<td align="left">2 和 4 位置都不正确</td>
</tr>
<tr>
<td align="left">0 3 2 4</td>
<td align="left">1A2B</td>
<td align="left">4 位置正确2 和 3 位置不正确</td>
</tr>
<tr>
<td align="left">5 6 7 8</td>
<td align="left">0A0B</td>
<td align="left">没有任何一个数字正确</td>
</tr>
<tr>
<td align="left">4 3 2 1</td>
<td align="left">0A4B</td>
<td align="left">4 个数字位置都不对</td>
</tr>
<tr>
<td align="left">1 2 3 4</td>
<td align="left">4A0B</td>
<td align="left">胜出 全中</td>
</tr>
<tr>
<td align="left">1 1 2 3</td>
<td align="left">输入不正确,重新输入</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">1 2</td>
<td align="left">输入不正确,重新输入</td>
<td align="left"></td>
</tr>
</tbody>
</table>
<p>答案在游戏开始时随机生成,只有 6 次输入的机会。每次猜测时,程序会给出当前猜测的结果,如果猜测错误,还会给出之前所有猜测的数字和结果以供玩家参考。输入时,用空格分隔数字。</p>
<p>针对猜数字游戏的需求,我们可以分解出如下任务:</p>
<ul>
<li>随机生成答案</li>
<li>判断每次猜测的结果</li>
<li>检查输入是否合法</li>
<li>记录并显示历史猜测数据</li>
<li>判断游戏结果。判断猜测次数,如果满 6 次但是未猜对则判负;如果在 6 次内猜测的 4 个数字值与位置都正确,则判胜</li>
</ul>
<p>当在为分解的任务编写测试用例时,不应针对被测方法编写单元测试,而应该<strong>根据领域场景进行编写</strong>,这也是为何测试驱动开发强调测试优先的原因。由于是测试优先,事先没有被测的实现代码,就可以规避这种错误方式。</p>
<p>编写测试的过程是进一步理解领域逻辑的过程,更是<strong>驱动</strong>我们去寻找领域概念的过程。由于在编写测试的时候,没有已经实现的类,这就需要开发人员<strong>站在调用者的角度去思考</strong>,即所谓“<strong>意图导向编程</strong>”。从调用的角度思考,可以驱动我们思考并达到如下目的:</p>
<ul>
<li>如何命名被测试类以及方法,才能更好地表达设计者的意图,使得测试具有更好的可读性;</li>
<li>被测对象的创建必须简单,这样才符合测试哲学,从而使得设计具有良好的可测试性;</li>
<li>测试使我们只关注接口,而非实现;</li>
</ul>
<p>在编写测试方法时,应遵循 Given-When-Then 模式这种方式描述了测试的准备、期待的行为以及验收条件。Given-When-Then 模式体现了 TDD 对设计的驱动力:</p>
<ul>
<li>当编写 Given 时,“驱动”我们思考被测对象的创建,以及它与其他对象的协作;</li>
<li>当编写 When 时,“驱动”我们思考被测接口的方法命名,以及它需要接收的传入参数;考虑行为方式,究竟是<strong>命令式</strong>还是<strong>查询式</strong>方法;</li>
<li>当编写 Then 时,“驱动”我们分析被测接口的返回值。</li>
</ul>
<p>例如,针对任务“判断每次的猜测结果”,我们<strong>首先要考虑由谁来执行此任务</strong>。从面向对象设计的角度来讲,这里的任务即“职责”,我们要找到职责的承担者。<strong>从拟人化的角度去思考所谓“对象”</strong>,就是要找到<strong>能够彻底理解understand该职责的对象</strong>。基于这样的设计思想,驱动我们获得了 Game 对象。进一步分析任务,由于我们需要判断猜测结果,这必然要求获知游戏的答案,从而寻找出表达了<strong>猜测结果</strong>这一领域知识的概念Answer这实际上就是以测试驱动的方式来帮助我们进行领域建模。</p>
<p>编写 When 可以帮助开发者思考类的行为,**一定要从业务而非实现的角度去思考接口。**例如:</p>
<ul>
<li>实现角度的设计check()</li>
<li>业务角度的设计guess()</li>
</ul>
<p>注意两个方法命名表达意图的不同,显然后者更好地表达了领域知识。</p>
<p>编写 Then 考虑的是如何验证,没有任何验证的测试不能称其为测试。由于该任务为判断输入答案是否正确,并获得猜测结果,因而必然需要返回值。从需求来看,只需要返回一个形如 xAxB 的字符串即可。通过 Given-When-Then 模式组成了一个测试方法所要覆盖的领域场景,而测试方法自身则以描述业务的形式命名。例如,针对“判断每次猜测的结果”任务,可以编写其中的一个测试方法:</p>
<pre><code class="language-java">@Test
public void should_return_0A0B_when_no_number_is_correct() {
//given
Answer actualAnswer = Answer.createAnswer(&quot;1 2 3 4&quot;);
Game game = new Game(actualAnswer);
Answer inputAnswer = Answer.createAnswer(&quot;5 6 7 8&quot;);
//when
String result = game.guess(inputAnswer);
//then
assertThat(result , is(&quot;0A0B&quot;));
}
</code></pre>
<p>测试方法名可以足够长,以便于清晰地表述业务。为了更好地辨别方法名表达的含义,我们提倡用 Ruby 风格的命名方法,即下划线分隔方法的每个单词,而非 Java 传统的驼峰风格。建议测试方法名以 should 开头,此时,默认的主语为被测类,即这里的 Game。因此该测试方法就可以阅读为Game should return 0A0B when no number guessed correctly。显然这是一条描述了业务规则的自然语言。</p>
<p>这三种方法各有风格,驱动领域场景的力量也各自不同,甚至这些方法在开发实践中并非处于同一个维度,然而在领域场景分析这个大框架下,又都直接或间接体现了场景的 6W 模型。当然,这里展现的仅仅是这些方法的冰山一角,讲解的侧重点还是在于通过这些方法来帮助我们提炼领域知识。同时,借助类似用例、用户故事、任务等载体,可以更加有效而直观地帮助我们理解问题域,抽象领域模型,从而为我们建立统一语言奠定共识基础。</p>
<h3>提炼领域知识</h3>
<p><strong>提炼领域知识需要贯穿整个领域驱动设计全过程</strong>,无论何时,都必须重视领域知识,并时刻维护统一语言。在进行领域场景分析时,这是一个双向的过程。一方面,我们已提炼出来的领域知识会指导我们识别用例,编写用户故事以及测试用例;另一方面,具体的领域场景分析方法又可以进一步帮助我们确认领域知识,并将在团队内达成共识的统一语言更新到之前识别的领域知识中。</p>
<p>这种双向的指导与更新非常重要,因为我们提炼的领域知识以及统一语言是领域模型的重要源头。“问渠那得清如许,为有源头活水来。”,只有源头保证了常新,领域模型才能保证健康,才能更好地指导领域驱动设计。</p>
<p>通过前面对用例、用户故事与测试驱动开发的介绍,我们发现这三个方法虽然都是领域场景分析的具体实现,但它们在运用层次上各有其优势。用例尤其是用例图的抽象能力更强,更擅长于对系统整体需求进行场景分析;用户故事提供了场景分析的固定模式,善于表达具体场景的业务细节;测试驱动开发则强调对业务的分解,利用编写测试用例的形式驱动领域建模,即使不采用测试先行,让开发者转换为调用者角度去思考领域对象及行为,也是一种很好的建模思想与方法。</p>
<p>在提炼领域知识的过程中,我们可以将这三种领域场景分析方法结合起来运用,在不同层次的领域场景中选择不同的场景分析方法,才不至于好高骛远,缺乏对细节的把控,也不至于一叶障目,只见树木不见森林。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/领域驱动设计实践(完)/009 运用领域场景分析提炼领域知识(上).md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/领域驱动设计实践(完)/011 建立统一语言.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":"70997e070d5a3cfa","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>