В прошлый раз мы рассмотрели фазы «красный / зеленый / рефакторинг» разработки через тестирование (TDD). На этот раз мы подробно рассмотрим преобразования, применяемые в фазе Грина.
Приоритетное преобразование
Большинство из вас слышали о рефакторингах, которые мы применяем на последней фазе TDD, но также имеются соответствующие стандартизированные изменения кода в зеленой фазе. Дядя Боб Мартин назвал их преобразованиями .
В Приоритете Преобразования (TPP) утверждается, что эти преобразования имеют свойственный порядок, и что преобразования выбора, которые выше в списке, приводят к лучшим алгоритмам.
Неподтвержденное свидетельство представлено на примере сортировки , где нарушение порядка приводит к пузырьковой сортировке, в то время как правильный порядок приводит к быстрой сортировке.
После некоторых изменений, основанных на сообщениях других людей, дядя Боб получил следующий упорядоченный список преобразований :
преобразование | Описание |
---|---|
{} -> ноль | нет кода вообще-> код, который использует ноль |
ниль> постоянная | |
далее константа> постоянная + | простая константа для более сложной константы |
далее константа> скалярная | замена константы переменной или аргументом |
Оператор-> заявления | добавив больше безусловных утверждений |
unconditional-> если | расщепление пути выполнения |
скалярная> массив | |
array-> контейнер | ??? этот никогда не используется и не объясняется |
Оператор-> хвостовой рекурсии | |
если-> в то время как | |
Оператор-> рекурсии | |
expression-> функция | замена выражения на функцию или алгоритм |
с переменной> Назначение | заменив значение переменной |
дело | добавление регистра (или еще) к существующему коммутатору или, если |
Применение ТЭС к римским цифрам ката
Чтение чего-либо дает лишь поверхностные знания, поэтому давайте попробуем TPP на небольшой знакомой проблеме: ката римских цифр .
Для тех из вас, кто не знаком с этим: цель состоит в том, чтобы перевести числа на римский язык. Смотрите таблицу слева для обзора римских символов и их значений.
Как всегда в TDD, мы начнем с самого простого случая:
1
2
3
4
5
6
7
8
|
public class RomanNumeralsTest { @Test public void arabicToRoman() { Assert.assertEquals( "i" , "i" , RomanNumerals.arabicToRoman( 1 )); } } |
Мы получаем это для компиляции с:
1
2
3
4
5
6
7
|
public class RomanNumerals { public static String arabicToRoman( int arabic) { return null ; } } |
Обратите внимание, что мы уже применили первое преобразование в списке: {}->nil
. Мы применяем второе преобразование, nil->constant
, чтобы добраться до зеленого:
1
2
3
4
5
6
7
|
public class RomanNumerals { public static String arabicToRoman( int arabic) { return "i" ; } } |
Теперь мы можем добавить наш второй тест:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
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
):
1
2
3
4
5
6
|
public static String arabicToRoman( int arabic) { if (arabic == 2 ) { return "ii" ; } return "i" ; } |
Однако это приводит к дублированию между числом 2
и числом возвращаемых я. Итак, давайте попробуем другую последовательность преобразований. Предупреждение: сейчас я перехожу в режим «Детские шаги».
Сначала сделайте constant->scalar
:
1
2
3
4
|
public static String arabicToRoman( int arabic) { String result = "i" ; return result; } |
Далее, statement->statements
:
1
2
3
4
5
|
public static String arabicToRoman( int arabic) { StringBuilder result = new StringBuilder(); result.append( "i" ); return result.toString(); } |
Теперь мы можем ввести if
без дублирования:
1
2
3
4
5
6
7
|
public static String arabicToRoman( int arabic) { StringBuilder result = new StringBuilder(); if (arabic >= 1 ) { result.append( "i" ); } return result.toString(); } |
А потом еще одно statement->statements
1
2
3
4
5
6
7
8
|
public static String arabicToRoman( int arabic) { StringBuilder result = new StringBuilder(); if (arabic >= 1 ) { result.append( "i" ); arabic -= 1 ; } return result.toString(); } |
И, наконец, мы делаем if->while
:
1
2
3
4
5
6
7
8
|
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), но являются разными версиями: один арабский и один римский.
Мы должны ввести класс для захвата этой концепции:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
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; } } |
Теперь получается, что у нас есть случай с завистью к функциям . Мы можем сделать это более очевидным, извлекая метод:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
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
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
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
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
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
как два отдельных параметра, но они не являются независимыми. Первый представляет часть арабского числа, еще не обработанную, в то время как последний представляет часть, которая обрабатывается. Итак, мы должны представить еще один класс для захвата общей концепции:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
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
дважды, а наших собственных членов — три раза, так что это не так уж плохо, но давайте подумаем об этом на минутку.
Имеет ли смысл сообщать римской цифре о процессе перехода с арабского на римский? Я думаю, что нет, поэтому давайте переместим код в нужное место:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
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
класс ArabicToRomanConversion
. Хм, может быть у рефакторингов тоже есть свой собственный порядок !
Хорошо, перейдем к нашему следующему тесту: 4. Мы можем сделать это с помощью другой серии преобразований. Сначала scalar->array
:
1
2
3
4
5
6
7
8
9
|
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
:
01
02
03
04
05
06
07
08
09
10
|
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
:
01
02
03
04
05
06
07
08
09
10
11
12
|
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
01
02
03
04
05
06
07
08
09
10
11
12
|
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
:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
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
:
01
02
03
04
05
06
07
08
09
10
11
|
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+
:
01
02
03
04
05
06
07
08
09
10
11
12
|
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
. Кстати, это должно быть константой:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
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(); } } |
Кроме того, похоже, у нас есть еще один случай зависти к функциям, который мы могли бы решить следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
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; } } |
ретроспективный
Первое, что я заметил, это то, что следование ТТП привело меня к открытию базового алгоритма намного быстрее, чем в некоторых из моих предыдущих попыток использовать этот ката. Следующая интересная вещь заключается в том, что между преобразованиями и рефакторингами существует взаимодействие. Вы можете либо выполнить преобразование, а затем очистить с помощью рефакторинга, или предотвратить необходимость рефакторинга, используя только те преобразования, которые не вводят дублирование. Выполнение последнего более эффективно, а также, по-видимому, ускоряет обнаружение требуемого алгоритма. Конечно, пища для размышлений. Кажется, что еще несколько экспериментов в порядке.
Ссылка: TDD и предпосылка приоритета преобразования от нашего партнера JCG Ремона Синнема в блоге по разработке безопасного программного обеспечения .