Рубрики
Без рубрики

Простые Ката: История Рефакторинга

Рефакторинг устаревшего кода может быть непростым делом. Чаще всего код не тестируется, дядя… Помеченный как рефакторинг, ката, java.

Рефакторинг устаревшего кода может быть непростым делом. Чаще всего код не тестируется, неясен и содержит некоторые ошибки. Если это не планируется, время, необходимое для рефакторинга, будет занимать все время, выделенное для функции. Затем эта функция будет реализована очень быстро, и это приведет к большему количеству ошибок в коде, который невозможно поддерживать.

В этом сообщении в блоге я попытаюсь показать некоторые инструменты и методы для безопасного рефакторинга фрагмента кода. Я буду использовать trivia kata в качестве поддержки для этого. Полученный код можно найти на GitHub .

Тест на характеристику: Золотой Мастер

Необходимым условием для любого рефакторинга является создание тестового жгута, чтобы быть уверенным, что рефакторинг ничего не нарушит. В этом примере казалось бы пустой тратой времени проверять все возможные пути с помощью модульных тестов. К счастью, код содержит множество трассировок, которые печатают то, что делает код. Затем можно запустить код с набором входных данных, захватить выходные данные и сохранить их. Этот вывод называется Golden Master . Каждый раз, когда код модифицируется, для запуска кода используются одни и те же входные данные, а выходные данные сравниваются с золотым мастером. Как вы, возможно, заметили, это не гарантирует, что код не содержит ошибок, это просто гарантирует, что код всегда ведет себя одинаково.

В Java золотой мастер может быть реализован с помощью библиотеки с именем approval :


    com.github.nikolavp
    approval-core
    0.3

Соответствующий тест показан ниже (см. commit ):

@Test
public void should_record_and_verify_golden_master() {
    String result = playGame(1L);

    Approvals.verify(result, Paths.get("src", "main", "resources", "approval", "result.txt")); // 2
}

private String playGame(long seed) {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    System.setOut(new PrintStream(outputStream)); // 3

    boolean notAWinner;
    Game aGame = new Game();

    aGame.add("Chet");
    aGame.add("Pat");
    aGame.add("Sue");

    Random rand = new Random(seed); // 1

    do {

        aGame.roll(rand.nextInt(5) + 1);

        if (rand.nextInt(9) == 7) {
            notAWinner = aGame.wrongAnswer();
        } else {
            notAWinner = aGame.wasCorrectlyAnswered();
        }

    } while (notAWinner);

    return new String(outputStream.toByteArray());
}

Поскольку код использует Random для броска кости, мы должны исправить начальное значение Случайный таким образом, он всегда возвращает одни и те же случайные броски в одном и том же порядке ( 1 ). Код должен быть запущен один раз с этим начальным значением, а выходные данные должны быть вставлены в src/main/resources/approval/result.txt для создания золотого мастера ( 2 ). После этого тест будет фиксировать выходные данные ( 3 ) и сравните его с золотым мастером. Если появится разница, появится запрос diff.

Иногда во время моих сеансов рефакторинга мне нравится намеренно совершать ошибки, просто чтобы проверить надежность моей тестовой системы. Я нахожу это очень полезным для повышения вашей уверенности в своих тестах и исправления их, если это необходимо.

Первый Шаг: Очистка

Лично я не могу по-настоящему думать, когда нахожусь перед раздутым и неясным кодом. Таким образом, первое, что я сделал для рефакторинга этого фрагмента кода, – это перестроил его, удалил неиспользуемый импорт и мертвый код, удалил магические строки и магические числа, использовал дженерики и уменьшил видимость полей и методов, которые можно уменьшить, чтобы быть уверенным, что я могу прикоснуться к ним, не нарушая общедоступный API. Большая часть этих рефакторингов может быть выполнена за вас на вашей СТОРОНЕ, так что не стесняйтесь делать это, так как это снижает риск ошибок и происходит намного быстрее. Вы также можете использовать подключаемый модуль типа SonarLint например, для обнаружения проблем непосредственно в вашей IDE.

Как только код стало легче обдумывать, я заметил, что строки использовались для определения категорий. Это показалось хорошим местом для создания перечисления (см. commit ). Для начала я просто заменил строки перечислением:

private void askQuestion() {
    if (currentCategory() == "Pop")
        print(popQuestions.removeFirst());
    if (currentCategory() == "Science")
        print(scienceQuestions.removeFirst());
    if (currentCategory() == "Sports")
        print(sportsQuestions.removeFirst());
    if (currentCategory() == "Rock")
        print(rockQuestions.removeFirst());
}

private String currentCategory() {
    if (places[currentPlayer] == 0) return "Pop";
    if (places[currentPlayer] == 4) return "Pop";
    if (places[currentPlayer] == 8) return "Pop";
    if (places[currentPlayer] == 1) return "Science";
    if (places[currentPlayer] == 5) return "Science";
    if (places[currentPlayer] == 9) return "Science";
    if (places[currentPlayer] == 2) return "Sports";
    if (places[currentPlayer] == 6) return "Sports";
    if (places[currentPlayer] == 10) return "Sports";
    return "Rock";
}

стал

private void askQuestion() {
    if (currentCategory().equals(Category.POP))
        print(popQuestions.removeFirst());
    if (currentCategory().equals(Category.SCIENCE))
        print(scienceQuestions.removeFirst());
    if (currentCategory().equals(Category.SPORTS))
        print(sportsQuestions.removeFirst());
    if (currentCategory().equals(Category.ROCK))
        print(rockQuestions.removeFirst());
}

private Category currentCategory() {
    if (places[currentPlayer] == 0) return Category.POP;
    if (places[currentPlayer] == 4) return Category.POP;
    if (places[currentPlayer] == 8) return Category.POP;
    if (places[currentPlayer] == 1) return Category.SCIENCE;
    if (places[currentPlayer] == 5) return Category.SCIENCE;
    if (places[currentPlayer] == 9) return Category.SCIENCE;
    if (places[currentPlayer] == 2) return Category.SPORTS;
    if (places[currentPlayer] == 6) return Category.SPORTS;
    if (places[currentPlayer] == 10) return Category.SPORTS;
    return Category.ROCK;
}

Такого рода оператор if , который очень похож на switch , почти всегда может быть заменен на Map так вот что я сделал (см. commit ). Я создал две карты, чтобы содержать вопросы каждой категории и категории для каждой должности:

private void askQuestion() {
    print(questionsByCategory.get(currentCategory()).removeFirst());
}

private Category currentCategory() {
   return categoriesByPosition.get(currentPosition());
}

Второй Шаг: Разделение

Второй шаг, которому я решил следовать, – это поместить определенную логику в частные методы. Еще раз, ваша IDE может помочь вам здесь, используйте ее!

Например, этот фрагмент кода:

print(players.get(currentPlayer) + " is getting out of the penalty box");

places[currentPlayer] = places[currentPlayer] + roll;
if (places[currentPlayer] >= NB_CELLS) {
    places[currentPlayer] = places[currentPlayer] - NB_CELLS;
}

print(players.get(currentPlayer) + "'s new location is " + places[currentPlayer]);

стал

print(players.get(currentPlayer) + " is getting out of the penalty box");
move(roll);
print(players.get(currentPlayer) + "'s new location is " + currentPosition());

Делая это, вы можете устранить дублирование, разложив алгоритмы (например, переместить игрока) на конкретные методы. Кроме того, вы подготавливаете поле для извлечения объектов из этого кода (см. Следующий раздел). Однако, чтобы сделать это правильно, частные методы должны быть в максимально чистой форме: они должны использовать или изменять как можно меньшее количество закрытых полей (см. commit ). Имея это в виду, следующий код:

private Category currentCategory() {
    return categoriesByPosition.get(currentPosition());
}

был заменен на

private Category currentCategory(int position) {
    return categoriesByPosition.get(position);
}

Третий шаг: Разделяй и властвуй

Теперь, когда логика была разделена на частные методы, довольно просто извлекать объекты из этих методов.

В этом ката мы увидели, что возникает несколько концепций: Игрок , Доска и Палуба вопросов . Также был добавлен класс PlayerList

Например, следующий код выводит следующий вопрос, который нужно задать:

public void roll(int roll) {

  // ...

  Category currentCategory = board.categoryOf(newPosition);
  print("The category is " + currentCategory);
  print(nextQuestionAbout(currentCategory));
}

private String nextQuestionAbout(Category category) {
  return questionsByCategory.get(category).removeFirst();
}

Создав объект Question Deck , мы можем получить такой код (см. commit ):

public void roll(int roll) {

  // QuestionDeck deck = new QuestionDeck(NB_QUESTIONS, CATEGORIES);

  Category currentCategory = board.categoryOf(newPosition);
  print("The category is " + currentCategory);
  print(deck.nextQuestionAbout(currentCategory));
}

Частный метод следующий Вопрос О и поле вопросы по категориям больше не находится в классе Game .

Когда вы извлекаете классы из основного кода, очень важно не нарушать общедоступный API. Однако некоторые методы можно аннотировать с помощью @Deprecated (см. commit ). Хороший пример этого можно увидеть в этом видео .

Четвертый шаг: Расстаться

Последний шаг – это когда мы удаляем все устаревшие методы, которые мы ввели во время рефакторинга. Не всегда возможно сделать это сразу после сеанса рефакторинга, поскольку у нас не всегда есть доступ к клиентскому коду, который использует наш общедоступный API. В данном случае мы так и делаем, поэтому я удалил эти методы (см. commit ).

Клиентский код выглядел следующим образом:

Game aGame = new Game();

aGame.add("Chet");
aGame.add("Pat");
aGame.add("Sue");

// aGame.roll()

Теперь класс Game больше не отвечает за вопросы, доску, категории и игроков, поэтому мы должны ввести их в конструктор.

private static final int NB_CELLS = 12;
private static final int NB_QUESTIONS = 50;
private static final List CATEGORIES = asList(Category.POP, Category.SCIENCE, Category.SPORTS, Category.ROCK);
private static final List PLAYERS = asList("Chet", "Pat", "Sue");

// ...
Game aGame = new Game(
        System.out,
        new Board(NB_CELLS, CATEGORIES),
        new QuestionDeck(NB_QUESTIONS, CATEGORIES),
        new PlayerList(PLAYERS)
);

// aGame.roll()

Подведение Итогов

Конечно, как это всегда бывает при рефакторинге, я мог бы сделать больше. Основной риск заключается в том, что больше не всегда означает лучше . Сеанс рефакторинга должен иметь определенную цель, и очень важно знать, когда остановиться. Это может быть, когда вы можете легко добавить свою новую функцию или исправить ошибку, или просто потому, что у вас больше нет времени, выделенного на эту задачу.

Я надеюсь, что этот пост дал вам некоторые инструменты и методы для рефакторинга вашего кода, не опасаясь что-то сломать.

Оригинал: “https://dev.to/rnowif/trivia-kata-a-refactoring-story-4bb9”