atdd和tdd
上一次 ,我们研究了测试驱动开发 (TDD)的红色/绿色/重构阶段。 这次,我们将详细研究在Green阶段应用的转换。
转型优先前提
你们中的大多数人都听说过我们在上一个TDD阶段中应用的重构,但是在Green阶段中也有相应的标准化代码更改。 鲍勃·马丁叔叔把它们命名为变换 。
转换优先级前提 (TPP)声称这些转换具有固有顺序,并且选择列表中较高的转换会导致更好的算法。
排序示例提供了轶事证据,其中违反顺序导致气泡排序,而正确顺序导致快速排序。
在根据其他人的帖子进行了一些修改之后,鲍勃叔叔得出了以下有序的转换列表 :
转型 | 描述 |
---|---|
{} –>无 | 完全没有代码->使用nil的代码 |
nil->常数 | |
常数->常数+ | 一个简单的常量到一个更复杂的常量 |
常数->标量 | 用变量或参数替换常量 |
声明->声明 | 添加更多无条件语句 |
无条件->如果 | 分割执行路径 |
标量->数组 | |
数组->容器 | ??? 从来没有使用过,也没有解释过 |
语句->尾递归 | |
如果->当 | |
语句->递归 | |
表达式->功能 | 用函数或算法替换表达式 |
变量->分配 | 替换变量的值 |
案件 | 将案例(或其他案例)添加到现有交换机,或者 |
将TPP应用于罗马数字Kata
阅读某些东西只会使您获得较浅的知识,因此让我们在一个小而熟悉的问题上尝试TPP:“ 罗马数字kata” 。
对于不熟悉它的人:目的是将数字转换为罗马字。 有关罗马符号及其值的概述,请参见左侧的表格。
与TDD中一样,我们从最简单的情况开始:
public class RomanNumeralsTest {
@Test
public void arabicToRoman() {
Assert.assertEquals("i", "i", RomanNumerals.arabicToRoman(1));
}
}
我们可以将其编译为:
public class RomanNumerals {
public static String arabicToRoman(int arabic) {
return null;
}
}
请注意,我们已经在列表上应用了第一个转换: {}->nil
。 我们将第二个转换nil->constant
应用于绿色:
public class RomanNumerals {
public static String arabicToRoman(int arabic) {
return "i";
}
}
现在我们可以添加第二个测试:
public class RomanNumeralsTest {
@Test
public void arabicToRoman() {
assertRoman("i", 1);
assertRoman("ii", 2);
}
private void assertRoman(String roman, int arabic) {
Assert.assertEquals(roman, roman,
RomanNumerals.arabicToRoman(arabic));
}
}
使此测试通过的唯一方法是引入一些条件( unconditional->if
):
public static String arabicToRoman(int arabic) {
if (arabic == 2) {
return "ii";
}
return "i";
}
但是,这导致数字2
和返回的i
数量重复。 因此,让我们尝试不同的转换序列。 警告:我现在要进入婴儿步骤模式。
首先,执行constant->scalar
:
public static String arabicToRoman(int arabic) {
String result = "i";
return result;
}
接下来, statement->statements
:
public static String arabicToRoman(int arabic) {
StringBuilder result = new StringBuilder();
result.append("i");
return result.toString();
}
现在我们可以介绍if
而不重复:
public static String arabicToRoman(int arabic) {
StringBuilder result = new StringBuilder();
if (arabic >= 1) {
result.append("i");
}
return result.toString();
}
然后另一个statement->statements
:
public static String arabicToRoman(int arabic) {
StringBuilder result = new StringBuilder();
if (arabic >= 1) {
result.append("i");
arabic -= 1;
}
return result.toString();
}
最后我们执行if->while
:
public static String arabicToRoman(int arabic) {
StringBuilder result = new StringBuilder();
while (arabic >= 1) {
result.append("i");
arabic -= 1;
}
return result.toString();
}
现在我们的测试通过了。 顺便说一句,对3的测试也是如此。
戴着重构帽,我们发现了一些更细微的重复:在数字1
和字符串i
。 它们都表达相同的概念(数字1),但是是不同的版本:一种阿拉伯语和一种罗马语。
我们应该引入一个类来捕获这个概念:
public class RomanNumerals {
public static String arabicToRoman(int arabic) {
StringBuilder result = new StringBuilder();
RomanNumeral numeral = new RomanNumeral("i", 1);
while (arabic >= numeral.getValue()) {
result.append(numeral.getSymbol());
arabic -= numeral.getValue();
}
return result.toString();
}
}
public class RomanNumeral {
private final String symbol;
private final int value;
public RomanNumeral(String symbol, int value) {
this.symbol = symbol;
this.value = value;
}
public int getValue() {
return value;
}
public String getSymbol() {
return symbol;
}
}
现在事实证明,我们有一个特征嫉妒的案例。 我们可以通过提取一种方法使这一点更加明显:
public static String arabicToRoman(int arabic) {
StringBuilder result = new StringBuilder();
RomanNumeral numeral = new RomanNumeral("i", 1);
arabic = append(arabic, result, numeral);
return result.toString();
}
private static int append(int arabic, StringBuilder builder,
RomanNumeral numeral) {
while (arabic >= numeral.getValue()) {
builder.append(numeral.getSymbol());
arabic -= numeral.getValue();
}
return arabic;
}
现在我们可以将append()
方法移动到RomanNumeral
:
public class RomanNumerals {
public static String arabicToRoman(int arabic) {
StringBuilder result = new StringBuilder();
RomanNumeral numeral = new RomanNumeral("i", 1);
arabic = numeral.append(arabic, result);
return result.toString();
}
}
public class RomanNumeral {
private final String symbol;
private final int value;
public RomanNumeral(String symbol, int value) {
this.symbol = symbol;
this.value = value;
}
public int getValue() {
return value;
}
public String getSymbol() {
return symbol;
}
public int append(int arabic, StringBuilder builder) {
while (arabic >= getValue()) {
builder.append(getSymbol());
arabic -= getValue();
}
return arabic;
}
}
我们可以通过内联现在仅在RomanNumeral
类中使用的getter来进一步清理:
public class RomanNumeral {
private final String symbol;
private final int value;
public RomanNumeral(String symbol, int value) {
this.symbol = symbol;
this.value = value;
}
public int append(int arabic, StringBuilder builder) {
while (arabic >= value) {
builder.append(symbol);
arabic -= value;
}
return arabic;
}
}
此代码还有另一个问题:我们将arabic
和builder
作为两个单独的参数传递,但它们不是独立的。 前者代表尚未处理的阿拉伯数字部分,而后者代表已处理的部分。 因此,我们应该引入另一个类来捕获共享概念:
public class RomanNumerals {
public static String arabicToRoman(int arabic) {
ArabicToRomanConversion conversion
= new ArabicToRomanConversion(arabic);
RomanNumeral numeral = new RomanNumeral("i", 1);
numeral.append(conversion);
return conversion.getResult();
}
}
public class RomanNumeral {
private final String symbol;
private final int value;
public RomanNumeral(String symbol, int value) {
this.symbol = symbol;
this.value = value;
}
public void append(ArabicToRomanConversion conversion) {
while (conversion.getRemainder() >= value) {
conversion.append(symbol, value);
}
}
}
public class ArabicToRomanConversion {
private int remainder;
private final StringBuilder result;
public ArabicToRomanConversion(int arabic) {
this.remainder = arabic;
this.result = new StringBuilder();
}
public String getResult() {
return result.toString();
}
public int getRemainder() {
return remainder;
}
public void append(String symbol, int value) {
result.append(symbol);
remainder -= value;
}
}
不幸的是,我们现在在RomanNumeral
有一个RomanNumeral
羡慕的案例功能。 我们两次使用conversion
,而我们自己的成员使用三次,所以这还不错,但是让我们考虑一下。
让罗马数字知道从阿拉伯到罗马的转换过程是否有意义? 我认为不是,所以让我们将代码移到正确的位置:
public class RomanNumerals {
public static String arabicToRoman(int arabic) {
ArabicToRomanConversion conversion
= new ArabicToRomanConversion(arabic);
RomanNumeral numeral = new RomanNumeral("i", 1);
conversion.process(numeral);
return conversion.getResult();
}
}
public class RomanNumeral {
private final String symbol;
private final int value;
public RomanNumeral(String symbol, int value) {
this.symbol = symbol;
this.value = value;
}
public String getSymbol() {
return symbol;
}
public int getValue() {
return value;
}
}
public class ArabicToRomanConversion {
private int remainder;
private final StringBuilder result;
public ArabicToRomanConversion(int arabic) {
this.remainder = arabic;
this.result = new StringBuilder();
}
public String getResult() {
return result.toString();
}
public void process(RomanNumeral numeral) {
while (remainder >= numeral.getValue()) {
append(numeral.getSymbol(), numeral.getValue());
}
}
private void append(String symbol, int value) {
result.append(symbol);
remainder -= value;
}
}
我们必须为RomanNumeral
的字段重新引入吸气剂,以便RomanNumeral
进行编译。 我们可以通过首先引入ArabicToRomanConversion
类来避免这种返工。 嗯, 也许重构也有一个固有的顺序 !
好,继续下一个测试:4.我们可以通过另一系列的转换使它通过。 首先, scalar->array
:
public static String arabicToRoman(int arabic) {
ArabicToRomanConversion conversion
= new ArabicToRomanConversion(arabic);
RomanNumeral[] numerals = new RomanNumeral[] {
new RomanNumeral("i", 1)
};
conversion.process(numerals[0]);
return conversion.getResult();
}
接下来, constant->scalar
:
public static String arabicToRoman(int arabic) {
ArabicToRomanConversion conversion
= new ArabicToRomanConversion(arabic);
RomanNumeral[] numerals = new RomanNumeral[] {
new RomanNumeral("i", 1)
};
int index = 0;
conversion.process(numerals[index]);
return conversion.getResult();
}
现在我们需要一个if
:
public static String arabicToRoman(int arabic) {
ArabicToRomanConversion conversion
= new ArabicToRomanConversion(arabic);
RomanNumeral[] numerals = new RomanNumeral[] {
new RomanNumeral("i", 1)
};
int index = 0;
if (index < 1) {
conversion.process(numerals[index]);
}
return conversion.getResult();
}
还有另一个constant->scalar
:
public static String arabicToRoman(int arabic) {
ArabicToRomanConversion conversion
= new ArabicToRomanConversion(arabic);
RomanNumeral[] numerals = new RomanNumeral[] {
new RomanNumeral("i", 1)
};
int index = 0;
if (index < numerals.length) {
conversion.process(numerals[index]);
}
return conversion.getResult();
}
您可能会看到前进的方向。 接下来是statement->statements
:
public static String arabicToRoman(int arabic) {
ArabicToRomanConversion conversion
= new ArabicToRomanConversion(arabic);
RomanNumeral[] numerals = new RomanNumeral[] {
new RomanNumeral("i", 1)
};
int index = 0;
if (index < numerals.length) {
conversion.process(numerals[index]);
index++;
}
return conversion.getResult();
}
然后if->while
:
public static String arabicToRoman(int arabic) {
ArabicToRomanConversion conversion
= new ArabicToRomanConversion(arabic);
RomanNumeral[] numerals = new RomanNumeral[] {
new RomanNumeral("i", 1)
};
for (RomanNumeral numeral : numerals) {
conversion.process(numeral);
}
return conversion.getResult();
}
最后是constant->constant+
:
public static String arabicToRoman(int arabic) {
ArabicToRomanConversion conversion
= new ArabicToRomanConversion(arabic);
RomanNumeral[] numerals = new RomanNumeral[] {
new RomanNumeral("iv", 4),
new RomanNumeral("i", 1)
};
for (RomanNumeral numeral : numerals) {
conversion.process(numeral);
}
return conversion.getResult();
}
现在我们已经完成了算法,我们要做的就是将其添加到numerals
数组中。 顺便说一句,这应该是一个常量:
public class RomanNumerals {
private static final RomanNumeral[] ROMAN_NUMERALS
= new RomanNumeral[] {
new RomanNumeral("iv", 4),
new RomanNumeral("i", 1)
};
public static String arabicToRoman(int arabic) {
ArabicToRomanConversion conversion
= new ArabicToRomanConversion(arabic);
for (RomanNumeral romanNumeral : ROMAN_NUMERALS) {
conversion.process(romanNumeral);
}
return conversion.getResult();
}
}
同样,这里似乎还有另一种令人羡慕的功能,我们可以通过以下方式解决:
public class RomanNumerals {
public static String arabicToRoman(int arabic) {
return new ArabicToRomanConversion(arabic).getResult();
}
}
public class ArabicToRomanConversion {
private static final RomanNumeral[] ROMAN_NUMERALS
= new RomanNumeral[] {
new RomanNumeral("iv", 4),
new RomanNumeral("i", 1)
};
private int remainder;
private final StringBuilder result;
public ArabicToRomanConversion(int arabic) {
this.remainder = arabic;
this.result = new StringBuilder();
}
public String getResult() {
for (RomanNumeral romanNumeral : ROMAN_NUMERALS) {
process(romanNumeral);
}
return result.toString();
}
private void process(RomanNumeral numeral) {
while (remainder >= numeral.getValue()) {
append(numeral.getSymbol(), numeral.getValue());
}
}
private void append(String symbol, int value) {
result.append(symbol);
remainder -= value;
}
}
回顾性
我注意到的第一件事是,遵循TPP之后,我发现该基本算法的速度比我在此kata上的一些较早尝试中要快得多。 下一个有趣的事情是,转换和重构之间似乎存在相互作用。 您既可以执行转换,然后进行重构清理,也可以仅使用不引入重复的转换来防止重构的需要。 后者的执行效率更高,而且似乎可以加快发现所需算法的速度。 当然值得深思。 似乎需要进行更多的实验。
参考: TDD和我们的JCG合作伙伴 Remon Sinnema 的“转换优先权前提”,来自“ 安全软件开发”博客。
翻译自: https://www.javacodegeeks.com/2013/01/tdd-and-the-transformation-priority-premise.html
atdd和tdd