在本系列的先前文章中,我解释了为什么一次性解决编码问题,就像对待僵尸一样,是一个错误。我正在使用一个有用的首字母缩略词来解释为什么以增量方式处理问题更好。 ZOMBIES 代表
Z – 零
O – 一
M – 多 (或更复杂)
B – 边界行为
I – 接口定义
E – 执行异常行为
S – 简单场景,简单解决方案
在本系列的前四篇文章中,我演示了前五个 ZOMBIES 原则。 第一篇文章实现了 Zero,它提供了通过代码的最简单路径。第二篇文章使用 One 和 Many 样本进行了测试,第三篇文章研究了Boundaries 和 Interfaces,第四篇检查了Exceptional behavior。 在本文中,我将研究首字母缩略词中的最后一个字母:S,它代表“简单场景,简单解决方案”。
简单场景,简单解决方案的实际应用
如果你回顾并检查为在本系列中实现购物 API 所采取的所有步骤,你将看到始终坚持最简单可能场景的有目的的决定。 在此过程中,你最终会得到最简单的解决方案。
这就是:ZOMBIES 通过坚持简单性来帮助你交付健壮、优雅的解决方案。
胜利了吗?
可能看起来你在这里完成了,一个不太认真的工程师很可能会宣布胜利。 但有见识的工程师总是会深入探究。
我一直推荐的一项练习是变异测试。 在你结束此练习并继续进行新的战斗之前,明智的做法是使用变异测试对你的解决方案进行良好的摇摆。 除此之外,你必须承认变异非常适合与僵尸作斗争。
使用开源 Stryker.NET 运行变异测试。

(Alex Bunardzic, CC BY-SA 4.0)
看起来你有一个幸存的变异体! 这不是一个好兆头。
这意味着什么? 就在你认为你有一个坚如磐石的解决方案时,Stryker.NET 告诉你,你的领域中并非一切都美好。
看看幸存的顽固变异体

(Alex Bunardzic, CC BY-SA 4.0)
变异测试工具采用了语句
if(total > 500.00) {
并将其变异为
if(total >= 500.00) {
然后它运行测试并意识到没有一个测试抱怨这种改变。 如果处理逻辑发生变化,并且没有一个测试抱怨这种改变,这意味着你有一个幸存的变异体。
为什么变异很重要
为什么幸存的变异体是麻烦的迹象? 这是因为你精心设计的处理逻辑控制着系统的行为。 如果处理逻辑发生变化,行为也应该改变。 如果行为发生变化,测试中编码的期望应该被违反。 如果这些期望没有被违反,这意味着这些期望不够精确。 你的处理逻辑中有一个漏洞。
要解决此问题,你需要“杀死”幸存的变异体。 你该怎么做? 通常,变异体幸存的事实意味着至少缺少一个期望。
查看你的代码,看看是否有任何缺失的期望
- 你明确定义了新创建的购物篮具有零个项目(并且,从含义上讲,总价为 0 美元)的期望。
- 你还定义了添加一个项目将导致购物篮中有一个项目的期望,如果该项目的价格为 10 美元,则总价为 10 美元。
- 此外,你定义了将两个项目添加到购物篮的期望,一个项目的价格为 10 美元,另一个项目的价格为 20 美元,总价为 30 美元。
- 你还声明了关于从购物篮中移除项目的期望。
- 最后,你定义了任何大于 500 美元的订单总额都会导致价格折扣的期望。 业务策略规则规定,在这种情况下,折扣是订单总价的 10%。
缺少什么? 根据变异测试报告,你从未定义当订单总额恰好为 500 美元时适用什么业务策略规则的期望。 你定义了订单总额大于 500 美元阈值时会发生什么,以及订单总额小于 500 美元时会发生什么。
定义此边缘情况期望
[Fact]
public void Add2ItemsTotal500GrandTotal500() {
var expectedGrandTotal = 500.00;
var actualGrandTotal = 450;
Assert.Equal(expectedGrandTotal, actualGrandTotal);
}
第一次尝试是伪造期望,使其失败。 你现在有九个微型测试; 八个成功,第九个测试失败
[xUnit.net 00:00:00.57] tests.UnitTest1.Add2ItemsTotal500GrandTotal500 [FAIL]
X tests.UnitTest1.Add2ItemsTotal500GrandTotal500 [2ms]
Error Message:
Assert.Equal() Failure
Expected: 500
Actual: 450
[...]
Test Run Failed.
Total tests: 9
Passed: 8
Failed: 1
Total time: 1.5920 Seconds
将硬编码的值替换为确认示例的期望
[Fact]
public void Add2ItemsTotal500GrandTotal500() {
var expectedGrandTotal = 500.00;
Hashtable item1 = new Hashtable();
item1.Add("0001", 400.00);
shoppingAPI.AddItem(item1);
Hashtable item2 = new Hashtable();
item2.Add("0002", 100.00);
shoppingAPI.AddItem(item2);
var actualGrandTotal = shoppingAPI.CalculateGrandTotal(); }
你添加了两个项目,一个价格为 400 美元,另一个价格为 100 美元,总计 500 美元。 在计算总价后,你期望它将是 500 美元。
运行系统。 所有九个测试都通过了!
Total tests: 9
Passed: 9
Failed: 0
Total time: 1.0440 Seconds
现在是关键时刻。 这个新的期望会移除所有变异体吗? 运行变异测试并检查结果

(Alex Bunardzic, CC BY-SA 4.0)
成功! 所有 10 个变异体都被杀死了。 干得好; 你现在可以放心地发布此 API 了。
结语
如果从这个练习中有一个收获,那就是熟练的拖延这一新兴概念。 这是一个至关重要的概念,要知道我们中的许多人倾向于盲目地冲进去设想解决方案,甚至在我们的客户完成描述他们的问题之前。
积极的拖延
拖延对于软件工程师来说并不容易。 我们渴望用代码弄脏我们的双手。 我们对众多设计模式、反模式、原则和现成的解决方案都了如指掌。 我们渴望将它们放入可执行代码中,并且我们倾向于大批量地执行它。 因此,控制住我们的冲动并仔细考虑我们所做的每一步确实是一种美德。
这个练习证明了 ZOMBIES 如何帮助你朝着解决方案迈出许多深思熟虑的小步骤。 了解并同意 Yagni 原则是 一回事,但在“战斗的激烈”中,那些深刻的考虑往往会飞出窗外,你最终会投入一切和洗碗槽。 这会产生臃肿、紧密耦合的系统。
迭代和增量
从这个练习中获得的另一个重要收获是认识到始终保持系统工作的唯一方法是采用迭代方法。 你通过应用一些返工来开发购物 API,也就是说,你通过更改已经更改的代码来进行编码。 在迭代解决方案时,这种返工是不可避免的。
许多团队遇到的问题之一是与迭代和增量相关的困惑。 这两个概念从根本上是不同的。
增量方法基于以下思想:你手中掌握一组清晰的需求(或蓝图),然后通过增量方式构建解决方案。 基本上,你逐个构建它,当所有部分都组装好后,你将它们放在一起,瞧! 解决方案就可以发布了!
相比之下,在迭代方法中,你不太确定你是否知道交付预期价值给付费客户所需知道的一切。 由于意识到这一点,你小心翼翼地前进。 你担心破坏已经工作的系统(即处于稳定状态的系统)。 如果你扰乱了这种平衡,你总是试图以最不具侵入性、最不具伤害性的方式扰乱它。 你专注于采用可以想象到的最小批量,然后快速完成每个批次的工作。 你更喜欢在几分钟内,有时甚至几秒钟内将系统恢复到稳定状态。
这就是为什么迭代方法如此频繁地坚持“假装直到你成功”。 你硬编码许多期望,以便你可以验证微小的更改不会阻止系统运行。 然后你进行必要的更改,以用实际处理替换硬编码的值。
根据经验,在迭代方法中,你的目标是以这样一种方式设计期望(微型测试),即它只促使对代码进行一项改进。 你逐项改进,并且每次改进,你都会执行系统以确保它处于工作状态。 当你以这种方式进行时,你最终会达到满足所有期望的阶段,并且代码以这样一种方式进行了重构,即它不会留下幸存的变异体。
一旦你达到这种状态,你就可以相当自信地发布解决方案了。
非常感谢无与伦比的 Kent Beck、Ron Jeffries 和 GeePaw Hill 一直激励着我踏上软件工程学徒的旅程。
愿你的旅程充满 ZOMBIES。
评论已关闭。