Trivia Kata:重构故事

重构遗留代码可能是一件棘手的事情。 通常,该代码未经测试,不清楚并包含一些错误。 如果未计划,则重构所需的时间将占用分配给功能的所有时间。 然后,该功能将很快实现,并且将导致更多无法维护的错误代码。

In this blog post, I will try to show some tools and methods to refactor safely a piece of code. I will use the trivia kata as a support for this. The resulting code can be found on GitHub.

Characterization Test: the Golden Master

任何重构的前提条件是构建测试工具,以确保重构不会破坏任何内容。 在这个例子中,用单元测试来测试所有可能的路径似乎是浪费时间。 幸运的是,该代码包含许多跟踪,可以打印出代码的作用。 然后可以使用一组输入来运行代码,捕获输出并保存。 此输出称为黄金大师。 每次修改代码时,都将使用相同的输入来运行代码,并将输出与黄金母版进行比较。 您可能已经注意到,这将无法确保代码没有错误,只会确保代码始终表现相同。

在Java中,可以使用称为认可:

<dependency>
    <groupId>com.github.nikolavp</groupId>
    <artifactId>approval-core</artifactId>
    <version>0.3</version>
</dependency>

The associated test is shown below (see 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());
}

由于代码使用了随机掷骰子,我们必须修复种子随机因此它总是以相同的顺序返回相同的随机掷骰(1个)。 该代码必须使用该种子运行一次,并且必须将输出粘贴到src / main / resources / approval / result.txt创建黄金大师(2)。 之后,测试将捕获输出(3),并将其与黄金大师进行比较。 如果出现差异,将提示差异。

有时在重构过程中,我喜欢故意出错,只是为了检查测试工具的可靠性。 我发现增加对测试的信心并在需要时进行修复非常有用。

First Step: Clean Up

Personally, I can’t really think when I am in front of bloated and unclear code. Thus, the first thing I did to refactor this piece of code is to rearrange it, delete unused imports and dead code, remove magic strings and magic numbers, use generics and reduce the visibility of fields and methods that can be reduced to be sure that I can touch them without breaking the public API. The major part of these refactorings can be made for you by your IDE so don’t hesitate to do it as it reduces the risk of error and it is much quicker. You can also use a plug-in like SonarLint for instance to detect issues directly into your IDE.

Once the code is easier to think about, I noticed that strings were used to identify categories. This seemed like a good place to create an enum (see commit). To start, I just replaced the strings with the enum:

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;
}

This kind of if statement that looks a lot like a switch can almost always be replaced by a Map so that is what I did (see commit). I created two maps to contain questions of each category and the category for each position:

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

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

Second Step: Segregate

我选择遵循的第二步是将特定逻辑放入私有方法中。 同样,您的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());

By doing that, you can remove duplication by factorizing algorithms (like move a player) into specific methods. Moreover, you prepare the field for extracting objects from this code (see the next section). However, in order to do that properly, the private methods have to be in the purest form possible: they should use or modify the least amount of private fields possible (see commit). With that in mind, the following code:

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

已被取代

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

Third Step: Divide and Conquer

现在,逻辑已被隔离为私有方法,从这些方法中提取对象非常简单。

在这个kata中,我们看到了几个概念:播放器,板和QuestionDeck。 一种播放器List还增加了一个类来处理玩家的轮换。 可以单独测试所有这些类,以便不再单独依赖黄金大师。

例如,以下代码输出下一个要询问的问题:

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();
}

By creating a QuestionDeck object, we can have a code like this (see 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));
}

私人方法nextQuestAbout和领域QuestionsByCategory不再在游戏类。

When you extract classes from the main code, it is very important not to break the public API. It is possible to annotate some methods with @Deprecated though (see commit). A good example of this can be seen in this video.

Fourth Step: Break up

The last step is when we remove all deprecated methods that we introduced during the refactoring. It is not always possible to do it right after the refactoring session as we don’t always have access to the client code that consumes our public API. In this case we do, so I removed these methods (see commit).

客户端代码如下所示:

Game aGame = new Game();

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

// aGame.roll()

现在游戏类不再负责问题,董事会,类别和参与者,因此我们必须将它们注入构造函数中。

private static final int NB_CELLS = 12;
private static final int NB_QUESTIONS = 50;
private static final List<Category> CATEGORIES = asList(Category.POP, Category.SCIENCE, Category.SPORTS, Category.ROCK);
private static final List<String> 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()

Wrapping Up

当然,与重构一样,我本可以做更多的事情。 主要的风险是更多并不总是意味着更好. A refactoring session must have a purpose and it is very important to know when to stop. It could be when you can add your new feature or fix a bug seamlessly or simply because you don’t have any 更多 time allocated to this task.

我希望本文能为您提供一些重构代码的工具和方法,而不必担心会破坏某些内容。

from: https://dev.to//rnowif/trivia-kata-a-refactoring-story-4bb9

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值