learn.lianglianglee.com/专栏/领域驱动设计实践(完)/083 测试优先的领域实现建模.md.html
2022-05-11 19:04:14 +08:00

1090 lines
49 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>083 测试优先的领域实现建模.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 class="current-tab" 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>083 测试优先的领域实现建模</h1>
<p>软件设计与开发的过程是不可分割的,那种企图打造软件工程流水线的代码工厂运作模式,已被证明难以奏效。探索设计与实现的细节,在领域模型驱动设计的过程中,设计在前、实现在后却也是合理的选择,毕竟二者关注的视角与目标迥然不同;但它并非瀑布式的一往向前,而是要形成分析、设计与实现的小步快走与反馈闭环,在多数时候甚至要将细节设计与代码实现融合在一起。</p>
<h3>建立稳定的领域模型</h3>
<p>不管设计如何指导开发,开发如何融合设计,都需要把握领域驱动设计的根本原则:<strong>以“领域”为设计的原点和驱动力</strong>。在领域设计建模时,务必不要考虑过多的技术实现细节,以免影响与干扰领域逻辑的设计。在设计时,让我们忘记数据库,忘记网络通信,忘记第三方服务调用。通过面向接口设计的形式,我们抽象出领域层需要调用的外部资源接口,统一命名为“南向网关”,即可在一定程度隔离业务与技术的实现,避免两个不同方向的复杂度产生叠加效应。</p>
<p>遵循整洁架构思想,我们希望最终获得的领域层对象并不依赖于任何外部设备、资源和框架。简言之,领域层的设计目标就是要达到<strong>逻辑层的自给自足</strong>,唯有不依赖于外物的领域模型才是最纯粹的、最独立的、最稳定的模型。</p>
<p>**这样的模型也是最容易执行单元测试的模型。**Michael C. Feathers 在《修改代码的艺术》一书中这样定义单元测试:“单元测试运行得快。运行得不快的测试不是单元测试。”他还进一步阐释:</p>
<blockquote>
<p>有些测试容易跟单元测试混淆起来。譬如下面这些测试就不是单元测试:</p>
<ul>
<li>跟数据库有交互</li>
<li>进行了网络间通信</li>
<li>调用了文件系统</li>
<li>需要你对环境作特定的准备(如编辑配置文件)才能运行的</li>
</ul>
</blockquote>
<p>显然,上述列举的测试都依赖了外部资源,它们实则属于测试策略中的集成测试。若测试不依赖外部资源,就可以运行得快,运行快才能快速反馈,并从通过的测试中获取信心。不依赖于外部资源的测试也更容易运行,遵守约束,就能够驱使我们开发出仅仅包含领域逻辑的领域模型,满足分层架构原则,实现业务关注点和技术关注点的分离。</p>
<h3>分层对象与测试策略</h3>
<p>领域驱动设计架构的每个逻辑层都定义了自己的控制边界,领域驱动设计的角色构造型位于不同层次。不同的设计元素,决定了它们不同的职责和设计的粒度。层次、职责和粒度的差异,恰好可以与测试策略形成一一对应的关系,如下图所示:</p>
<p><img src="assets/2dfe67c0-fe35-11e9-8dbf-c384fd54ec6b" alt="72439366.png" /></p>
<p>左侧通过六边形架构来清晰表达不同的层次。位于基础设施层中的远程服务担负的主要作用是与跨进程客户端之间的交互,强调服务提供者与服务消费者之间的履约行为。在这个层面上,我们更关心服务的契约是否正确,保护契约以避免它的变更引入缺陷,故而需要为远程服务编写契约测试。</p>
<p>业务核心在应用层与领域层。应用层的应用服务对应于一个领域场景,它遵循了整洁架构思想,通过网关角色构造型隔离了对外部资源的访问。遵循领域驱动设计对应用层的期望,需要设计为粗粒度的应用服务。它承担了外观服务的职责,并不真正包含具体的领域逻辑,为其编写集成测试是非常合理的选择。</p>
<p>场景驱动设计在分配职责时要求将不依赖于外部资源的原子任务分配给聚合内的领域模型对象这些原子任务都是自给自足的领域行为为其编写单元测试非常容易。凡是需要访问外部资源的行为都被推向了处理组合任务的领域服务构成了更加完整的领域行为同样需要编写单元测试来保护它。由于领域服务可能会与访问外部资源的网关角色构造型协作因此需要引入模拟Mock框架编写单元测试。聚合内的领域模型对象优先承担了领域行为既避免了过程式的贫血模型又能保证单元测试减少不必要的模拟。<strong>单元测试保护下的领域核心逻辑,是企业系统的核心资产</strong>,它确保了领域逻辑的正确性,允许开发人员安全地对其进行重构,使得领域模型能够在稳定内核的基础上具有了持续演化的能力。</p>
<p>测试不仅为系统创建了一张保护网编写良好的测试更是一份演进的、鲜活的文档Living Document。由于领域层的领域模型对象真实完整地体现了领域概念为了避免团队成员对这些领域概念产生不同理解除了需要在统一语言的指导下定义领域模型对象之外最好还需要一种简洁的方式来表达和解释领域尤其是核心领域Core Domain。Eric Evans 提出用“精炼文档”来描述和解释核心领域,他说道:</p>
<blockquote>
<p>这个文档可能很简单,只是最核心的概念对象的清单。它可能是一组描述这些对象的图,显示了它们最重要的关系。它可能在抽象层次上或通过示例来描述基本的交互过程。它可能会使用 UML 类图或时序图、专用于领域的非标准的图、措辞严谨的文字解释或上述这些元素的组合。</p>
</blockquote>
<p>尝试使用测试作为这样的“精炼文档”,或许会有更大的惊喜。一方面,你无需格外为核心领域编写单独的精炼文档,引入单元测试或者采用测试驱动开发就能自然而然收获完整的测试用例;另一方面,这些测试更加真实地体现了领域模型对象之间的关系,包括它们之间的组合与交互过程。将测试作为“精炼文档”还能保证领域模型的正确性,甚至可以更早帮助设计者发现设计错误。软件设计本身就是一个不断试错的过程,借助事件风暴与场景驱动设计可以让设计过程变得清晰简单,具备可视化的能力,但它终归不是代码实现,时序图以及时序图脚本体现的也仅仅是留存在脑海中的一种交互模式罢了。</p>
<p>那么,将设计通过代码来实现,就能确保设计没有问题吗?未必如此,实现代码仅仅是对设计方案的一种体现和落地,缺乏对运行结果的检测。<strong>检验设计正确性的关键标准是编写测试</strong>,其中,单元测试由于反馈更加及时快速,是最为重要的<strong>验证</strong>手段。</p>
<h3>测试驱动开发的实现建模</h3>
<p>从设计到实现是一个不断<strong>沟通</strong>的过程这个沟通不仅仅指团队中不同角色成员之间的沟通还包括代码的实现者与阅读者之间的沟通。这种沟通并非面对面除非采用结对编程而是藉由代码这种“媒介”产生一种穿越时空的沟通形式。之所以强调代码的沟通作用在于对维护成本的考量。Kent Beck 说:“在编程时注重沟通还有一个很明显的经济学基础。软件的绝大部分成本都是在第一次部署以后才产生的。从我自己修改代码的经验出发,我花在阅读既有代码的时间要比编写全新的代码长得多。如果我想减少代码所带来的开销,我就应该让它容易读懂。”</p>
<p>要做到让代码易懂需要保持代码的简单。少即是多有时候删掉一段代码比增加一段代码更难相应的它带来的价值很可能比后者更高。许多程序员常常感叹开发任务繁重每天要做的工作加班也做不完与此同时们又在不断地臆想功能的可能变化由此堆砌更为复杂的代码。明明可以直道行驶偏偏要以迂为直增加不必要的间接层然后美其名曰保证系统的可扩展性只可惜这样的可扩展性设计往往在最后会沦为过度设计。Neal Ford 在《卓有成效的程序员》一书中将这种情形称之为“预想开发Speculative Development”。预想开发会事先设想许多可能需要实现的功能这就好比是“给软件贴金”程序员一不小心就会跳进这个迷人的陷阱。</p>
<p>Kent Beck提倡极限编程eXtreming ProgrammingXP他认为程序员应追求<strong>简单</strong>的价值观。他强调:“在各个层次上都应当要求简单。对代码进行调整,删除所有不提供信息的代码。设计中不出现无关元素。对需求提出质疑,找出最本质的概念。去掉多余的复杂性后,就好像有一束光照亮了余下的代码,你就有机会用全新的视角来处理它们。”编写代码易巧难工,卖弄太多的技巧往往会导致业务真相被掩埋在复杂的代码背后。</p>
<p>场景驱动设计从领域场景出发来驱动设计,目的就是希望能给出恰如其分的设计模型。若要在领域实现建模阶段,能够及时验证设计的正确性,确保代码的沟通作用,体现从设计到实现一脉相承的简单性,就可以考虑<strong>测试驱动开发</strong></p>
<p>测试驱动开发是一种测试优先的编程实现方法。作为极限编程的一种开发实践,从十余年前 Kent Beck 提出这一方法至今,该方法仍然饱受争议,许多开发人员仍然无法理解:在没有任何实现的情况下,如何开始编写测试?这实际上带来一个问题的思考:<strong>为什么需要测试优先?</strong></p>
<p>在进行软件设计与开发的过程中,每个开发者其实都会扮演两个角色:</p>
<ul>
<li>接口的调用者</li>
<li>接口的实现者</li>
</ul>
<p>所谓“设计良好的接口”就是让调用者用起来很舒服的接口使用简单不需要了解太多的知识接口清晰表达意图。要设计出如此良好的接口就需要站在调用者角度而非实现者角度去思考接口。编写测试其实就是在编程实现之前假设对象已经有了一个理想的方法接口符合调用者的期望能够完成调用者希望它完成的工作而又无需调用者了解太多的信息。实际上这也是意图导向编程Programming by Intention思想的体现。</p>
<p>测试驱动开发的一个常见误区是,测试驱动开发没有设计,一开始就要编写测试代码。事实上,测试驱动开发强调的“测试优先”,其实质是要求<strong>需求分析优先</strong>,对需求对应的领域场景进行拆分,就是<strong>任务分解优先</strong>。因此,开发人员不应该从一开始就编写测试,而是分析需求(常常是用户故事),识别出可控粒度的领域场景,对其进行<strong>任务分解</strong>。对任务的分解其实就是对职责的识别,且识别出来的职责在被分解为单独的任务时,必须是可验证的。如此过程,不正是场景驱动设计过程要求的吗?</p>
<p>我们可以将场景驱动设计与测试驱动开发结合起来。分解任务是场景驱动设计的核心步骤,通过它进一步理清了领域场景,以便于将职责分配给合适的角色构造型,这是一个由外至内方向的设计过程;分解的任务又可以进一步划分为多个可以验证的测试用例,然后按照测试—实现—重构的节奏开始编码实现,从最容易编写单元测试的聚合内领域模型对象开始,再到领域服务,这是一个由内至外方向的开发过程。</p>
<p>由于场景驱动设计已经进行了任务分解,获得了时序图脚本,在进入领域实现建模时,就可以非常自然地采用测试驱动开发。首先挑选分解好的任务,从履行原子任务的聚合对象开始。如前所述,聚合承担了自给自足的领域行为,因此不需要考虑任何外部资源和技术实现,仅需要针对领域逻辑编写测试方法即可。只要将该任务的领域逻辑分解为细粒度的测试用例,就可以开始编写测试。显然,场景驱动设计与测试驱动开发皆以“分解任务”作为重要的设计和开发驱动力,从任务到测试用例,再到测试编写,非常顺畅地实现了领域设计建模到领域实现建模的无缝衔接:</p>
<p><img src="assets/b58da9c0-fe36-11e9-a8ef-c5670dd28542" alt="51612272.png" /></p>
<p>测试驱动开发非常强调节奏感。所谓“测试—实现—重构”,就是“红—绿—黄”的节奏。通过长期练习培养的开发节奏可以让编码行为变得更加高效、条理、清晰。如果将用户故事的验收标准、场景驱动设计、持续集成与测试驱动开发结合起来,就是一个迭代周期内增量开发的全过程:</p>
<ul>
<li>领取用户故事,与需求分析人员、测试人员沟通需求和验收标准</li>
<li>识别领域场景,进行任务分解</li>
<li>根据分解的任务确定测试用例</li>
<li>从业务角度编写测试方法</li>
<li>思考由哪个类承担接口方法,由此驱动出类</li>
<li>按照 Given-When-Then 模式编写测试,由此驱动出方法接口</li>
<li>编译无法通过,由此定义被测类和方法</li>
<li>运行测试,红色,表示测试未通过</li>
<li>编写恰好让测试通过的实现代码,让测试变成绿色</li>
<li>分辨产品代码和测试代码是否存在坏味道,若有,重构之</li>
<li>记得重构之后还要运行测试,确定测试通过</li>
<li>本地运行构建,满足提交条件后,提交代码</li>
<li>待持续集成通过后,开始编写新的测试</li>
</ul>
<p>在创建测试类时,是驱动出类的时机;按照 Given-When-Then 模式编写测试时,是驱动方法接口的时机。若已采用场景驱动设计,结合领域设计模型和角色构造型确定了履行职责的领域模型对象,并通过时序图脚本确定了协作方式和方法接口,会在一定程度上降低测试驱动开发的设计驱动价值,但也让整个测试驱动开发过程更加顺畅。</p>
<h3>测试驱动开发三定律</h3>
<p>要培养测试驱动开发的节奏感需要理清测试—实现—重构三者之间的关系。Robert Martin 分析了三者之间的关系,将其总结为测试驱动开发三定律:</p>
<ul>
<li>定律一:一次只写一个刚好失败的测试,作为新加功能的描述</li>
<li>定律二:不写任何产品代码,除非它刚好能让失败的测试通过</li>
<li>定律三:只在测试全部通过的前提下,做代码重构,或开始新加功能</li>
</ul>
<h4>定律一</h4>
<p>新功能是由新测试驱动出来的,没有编写测试,就不应该增加新功能,而现有代码已经由测试保证,增强了迈向新里程的信心。测试方法是对功能的描述,每个测试方法只做一件事情。测试方法应命名为表达该功能的自然语言,例如针对待测试功能“为合同分配一个自定义的唯一编号”,就可以定义测试方法:</p>
<pre><code class="language-java">@Test
public void should_assign_unique_customized_number_for_contract() {}
</code></pre>
<p>在编写测试驱动新功能时,开发者扮演的角色是接口的调用者,因此,一个<strong>刚好失败</strong>的测试,表达了调用者不满于现状的诉求,而且这个诉求非常简单,就好似调用者为实现者设定的一个具有明确针对性的小目标,轻易可以达成。如果采用结对编程,就可以分别扮演调用者和实现者的角色,专注于自己的视角,让测试驱动开发的过程进展更加顺利。定律一要求<strong>一次只写一个</strong>测试,则是为了保证整个开发过程的小步快行,做到步步为营。</p>
<h4>定律二</h4>
<p>一个失败的测试,意味着需要增加新功能;让测试刚好通过,是实现者唯一需要达成的目标。这就好似玩游戏一样,测试的编写者确定了完成游戏的目标,然后由此去设定每一关的关卡。游戏的玩家不要好高骛远,应以通过当前游戏关卡为己任,而不要像打斯诺克那样,每击打一个球,还要去考虑击打的球应该落到哪个位置,才有利于击打下一个球。一次只通一关,让测试<strong>刚好</strong>通过,就能让实现者的目标很明确,达到简单、快速、频繁验证的目的。</p>
<p>只要测试通过了,就不要编写任何产品代码,保证所有编写好的产品代码都在测试的保护下。编写任何超越让测试刚好通过的产品代码,都可以视为是过度设计。这就要求测试驱动开发的开发者克制追求大而全的野心,谨守住“只要求测试恰好通过足矣”的底线,不写任何额外的或无关的产品代码,保证实现方案的简单。</p>
<h4>定律三</h4>
<p><strong>测试全部通过</strong>意味着目前开始了的功能都已被实现,但未必完美。这个时候开始<strong>重构</strong>,在保证既有功能外部行为不变的前提下,安全地对代码设计做出优化,去除坏味道。每执行一步重构,都要运行一遍测试,保证重构操作没有破坏已有功能。这样就能做到及时而安全的重构,重构的代价也会变得更小。</p>
<p>添加新功能与重构在同一时刻不共存,要么添加新功能,要么重构,不可同时进行。在全部测试已经通过的情况下,若发现产品代码和测试代码存在坏味道,应该先进行重构,再考虑添加新功能。</p>
<p>测试驱动开发三定律对红绿黄的开发节奏提出了规范要求,就好似我们驾驶汽车需要遵守红绿黄灯的交通规则一般。只要严格遵循三定律进行测试驱动开发,就能做到已有产品代码的行为全被测试保证;功能的实现做到了尽可能简单,满足客户的需求;产品代码和测试代码都容易理解,没有坏味道。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/领域驱动设计实践(完)/082 事件溯源模式.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/领域驱动设计实践(完)/084 深入理解简单设计.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":"70997ebdd92f3cfa","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>