Статьи

TDD и предпосылка приоритета трансформации

В прошлый раз мы рассмотрели фазы «красный / зеленый / рефакторинг» разработки через тестирование (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 Ремона Синнема в блоге по разработке безопасного программного обеспечения .