TDD入门首选,Bowling Kata

保龄球

Bowling Game是一道经典的编程Kata,而且是Bob大叔推荐的Kata,那么它究竟有什么样的魅力呢?这次的星云道场就和大家一起来说道说道。

题设

保龄球记分牌

保龄球比赛一局有10轮,每轮选手有两次投球机会来击倒全部10个球瓶。如果第一球就击倒全部球瓶,称之为“strike”,用“x”表示,得分为10分再加下两球的倒瓶数;如果第一球未全中则再打一球,当第二球击倒剩余球瓶,称之为“spare”,用“/”表示,得分为10分再加下轮第一球的倒瓶数;如第二球也未击倒所有球瓶,称之为“miss”,用“-”表示,得分就是两次击倒球瓶之和。

当比赛到第10轮时获得三次投球机会,如第一次全中,仍可继续投完剩余两个球;如果补中,则继续投完剩余一球;如果两次投球未击倒全部球瓶,则比赛结束。计算得分最高者则为优胜,计分范例如下:

  • X X X X X X X X X X X X (12 投,12 全中) = 10轮 * 30分 = 300分
  • 9- 9- 9- 9- 9- 9- 9- 9- 9- 9- (20 投,10 次失误,每次9分) = 10轮 * 9分 = 90分
  • 5/ 5/ 5/ 5/ 5/ 5/ 5/ 5/ 5/ 5/5 (21投,10次补中,第一球均为5分,第10轮第三投也为5分) = 10轮 * 15分 = 150分

分析

题设围绕保龄球规则展开,初看不难,但如果深究题设就会马上陷入一些“重要”的细节之中,比如一轮比赛中击中的球瓶数量,这完全就是一个随机值;再比如如何计算本轮比赛得分,似乎也不容易,因为很有可能与后面的投球相关,可后面的投球却也是随机行为。这样的问题一个接着一个,很快就占据了全部大脑细胞。

无法思考

虽然上面这些问题在完成这道Kata时,可能都是需要考虑的,不过这并不意味着它们就是Bowling Kata的关注焦点,实际上它们确实不是。尝试将上述Bowling需求进行分解,就能得到两个最明显的要求:

  • 统计每轮比赛投球击中的球瓶数,也就是说击中的球瓶只是输入值,在合理范围内即可
  • 计算一局比赛结束后的得分,因此不必考虑那些还未进行的轮次

因此考虑设计一个具有上述两个功能的类来表达,其中roll方法用于记录每次投球击中的球瓶数量,score方法用于计算总分。

public class BowlingGame {
    public void roll(int pins) {...}

    public int score() {...}
}

实现

从上述分析可知,Bowling需要记录所有的投球结果才能计算获得分数。考虑几种投球结果的极端情况可得:

  • 10轮均为全中,则产生12(10 + 2)个投球结果
  • 10轮均为补中,则产生21(10 * 2 + 1)个投球结果
  • 10轮均为未全部击中,则产生20(10 * 2)个投球结果

因此简单设计一个满足上述边界情况下的击中球瓶的存储。

public class BowlingGame {
    private int rolls[] = new int[21];
    private int rollIndex = 0;

    public void roll(int pins) {
        rolls[rollIndex++] = pins;
    }
}

接着就可以采用TDD的方式开始逐个实现各种记分规则了,先来一个最简单的。

@Test
public void should_be_zero_when_no_pins_knocked_down() throws Exception {
    // given
    for (int i = 0; i < 20; i++) {
        game.roll(0);
    }
    // when
    // then
    assertThat(game.score(), is(0));
}

这个实现不会有太大的难度,简单快速地实现就可以了,并且该实现对于每轮未能击中所有球瓶的情况都可适用。

public void roll(int pins)  {
    rolls[rollIndex++] = pins;
}

public int score()  {
    int score = 0;
    for (int r : rolls)  {
        score += r;
    }
    return score;
}

在这个基础上试试出现补中的场景,对原有用例简单调整即可获得。

@Test
public void should_score_spare() throws Exception {
    // given
    game.roll(9);
    game.roll(1);
    for (int i = 0; i < 18; i++) {
        game.roll(4);
    }
    // when
    // then
    assertThat(game.score(), is(86));
}

之前的实现无法满足用例是因为补中的场景中,分数的计算不再是两次投球击中的球瓶之和,而是在这个基础上增加下一次投球的击中球瓶数作为奖励,按此思路首先调整原有实现。

public int score()  {
    int score = 0;
    int index = 0;
    for (int frame = 0; frame < 10; frame++)  {
        score += roll[index] + roll[index + 1];
        index += 2;
    }
    return score;
}

在此基础上增加对补中场景的判断以及得分计算方法。

public int score()  {
    int score = 0;
    int index = 0;
    for (int frame = 0; frame < 10; frame++)  {
        if (rolls[index] + rolls[index + 1] == 10) {
            score += 10 + rolls[index + 2];
            index += 2;
        } else {
            score += rolls[index] + rolls[index + 1];
            index += 2;
        }
    }
    return score;
}

有了前两种场景的实现,实现全中场景就可以以此类推,照例先给出测试用例。

@Test
public void should_score_strike() throws Exception {
      // given
      rollStrike();
      rolls(18, 4);
      // when
      int score = game.score();
      // then
      assertThat(score, is(90));
}
public int score() {
    int score = 0;
    int index = 0;
    for (int frame = 0; frame < 10; frame++) {
        if (isStrike(index)) {
            score += 10 + strikeBonus(index);
            index += 1;
        }else if (isSpare(index)) {
            score += 10 + spareBonus(index);
            index += 2;
        } else {
            score += missScore(index);
            index += 2;
        }
    }
    return score;
}

通过上面的几次迭代Bowling要求的功能就实现了,当然这种实现比较粗旷,基本上是遇到一个问题解决一个问题,没有过多的预先设计,而是在代码实现的过程中逐步地完善整个设计。

思考

为何Bob大叔要推荐Bowling,因为它足够简单不需要太多复杂的技术,运用的都是编程语言中最最基本的概念,无论是何种语言的初学者都能胜任,绝对的入门经典;同时Bowling又足够的丰富,因为它的题设是递进式的,非常适合通过TDD迭代地实现,从简入繁逐层推进,另外它还能练习重构,可以比较容易地把clean code中的一些基础原则运用在里面;最后它又是能够启发思考的,当你完成这道Kata之后你就发现对于需求关注点、职责的把握可以避免过早地陷入各种零散又相互交织的细节中,于是如何更好地把握需求、澄清需求(清楚要做什么或不做什么)就是平日工作中的首要任务了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 保龄球规则: 一局比赛共有 10 个计分格。选手在每一格里有 2 次机会击倒所有 10 个球瓶。如果球手在一个计分...
    _Raye阅读 6,907评论 0 0
  • Java Web阶段附加 题目1 自己实现一个功能类似于ArrayList的容器MyArrayList,MyArr...
    迷茫o阅读 99评论 0 0
  • 太阳当空,一路上,跟我走。 而,这个太阳呢,就一个,全球。 吹牛。 不是说有七个太阳,被后羿射下了六个,才剩下这一...
    鲁长安阅读 180评论 0 0
  • 我们在谈论论文投稿的事情时,师兄分享了这样一件事情: 作为一名博士,师兄一直跟着导师在认真地做着实验。但是,当论文...
    梦想搬砖者阅读 486评论 0 0
  • 学习就象打坐,没有定力就很难集中意念,思想就时不时会受到各种骚扰而出轨。 怎么办? 立即拉回来,继续。
    自由的代驾阅读 143评论 0 4