[阅读] 敏捷软件开发 —— 敏捷开发(二)

本贴最后更新于 2255 天前,其中的信息可能已经时移世异

快一周过去了,再次拿起这本书。周六是美好的日子,没有学校的课程,不用担心作业,没有人约,也不用去考虑太多其他琐事,只需要静下来看会儿书,多么惬意美好。

回顾

总得回顾一下上周看了些什么的,然而写读后感的好处莫过于不用再去翻书啦。看看自己上周写的文章,文字不多不少,但是也是体会颇深,也会有一些不同的体会。

  • 敏捷是有组织的,是团队性的。
  • 敏捷开发方法:极限编程。
  • 用户素材 与 任务计划。
  • 测试驱动开发,从无到有的构建。
  • 重构是必要的。

今天开始学习第六章,一次编程实战,长文预警!

一次编程实战

设计和编程都是人的活动,忘记了这一点,将会失去一切。

这一章节,是一次 XP 的编程的实践,章节中采用的是以对话的形式展现,相比于无聊的阐述,以故事的方式却好了很多。汲取朋友的经验,在这之前先去最后一页翻看了 保龄球比赛 规则,了解了规则后再看看确实棒很多!但是发现光看的话不能全身心的投入进去,所以准备实践一番。

在这里,我将我所学习到的分为连个部分来总结,不过在那之前,还是需要了解的应该是保龄球规则~~~

保龄球规则

保龄球是一种比赛,比赛者把一个哈密瓜大小的球顺着一条窄窄的球道投向 10 个木瓶。目的是在每次投球中击倒尽可能多的木瓶。

一局比赛由 10 轮组成。每轮开始,10 个木瓶都是竖立摆放的。比赛者可以投球两次,尝试击倒所有木瓶。

如果比赛者在第一次投球中就击倒了所有木瓶,称之为“全中”,并且本轮结束。

如果比赛者在第一次投球中没有击倒所有木瓶,但在第二次投球中成功击倒了所有剩余的木瓶,称之为“补中”。一轮中第二次投球后,即使还有未被击倒的木瓶,本轮也宣告结束。

  1. 全中轮的记分规则为:10,加上接下来的两次投球击倒的木瓶数,再加上前一轮的得分。
  2. 补中轮的记分规则为:10,加上接下来的一次投球击倒的木瓶数,再加上前一轮的得分。
  3. 其他轮的记分规则为:本轮中两次投球所击倒的木瓶数,加上前一轮的得分。

如果在第 10 轮全中,那么比赛者可以再多投球两次,以完成对全中的记分。同样,如果第 10 轮为补中,那么比赛者可以再多投球一次,以完成对补中的记分。因此,第 10 轮可以包含 3 次投球而不是 2 次。

  • 保龄球:bowling
  • 木瓶:ball
  • 局:game
  • 轮:frame
  • 全中:strike
  • 补中:spare

测试驱动开发

两个人的 XP 编程,一起商讨需求,在确定好了以后,每次首先进行的就是编写测试用例。主人公分别是 RSK 和 RCM,以下简称 S 和 C。

普通情况

对于这个实战,我们需要明白 输入输出 是什么。

  • 输入: 一个投掷(throw)的序列
  • 输出:每一轮(Frame)的得分

然而,S 提出对于 Throw 类并不需要测试:

关注有实际行为的对象,而不是仅仅存储数据的对象。

很明显,throw 不过是一个存储数据的对象。所以,将目光移至依赖链上的 Frame 类,所以,为他编写测试用例。

  • 测试用例

    @Test void testScoreNoThrows(){ Frame frame = new Frame(); frame.add(5); assertEquals(5, frame.getScore()); }
  • 编译通过

    public class Frame { private int itsScore = 0; public void add(int pins) { } public int getScore() { return 0; } }
  • 测试通过

    public class Frame { private int itsScore = 0; public void add(int pins) { itsScore += pins; } public int getScore() { return itsScore; } }

这个时候,最最最基本的要求就达到了。但是对于 add 方法,是十分脆弱的,当参数为 11 的时候,就会出现预料之外的情况。但是现在其实并不需要太过多的考虑,我们首先做的不过是基础的进球能够实现。

这时 C 提出,现在的代码却有一个问题,我们以 一轮 为单位,但是保龄球比赛是有十轮的,当进行到后面几轮的时候,调用 getScore 是没有意义的,因为一个 Frame 只代表了一轮。而且,当计算总分的时候还需要将所有的 Frame 给一起计算起来,是十分繁琐的,那么我们希望的是什么呢? —— Frame 之间互相知晓,而谁会持有这些不同的 Frame 对象呢?那应该上升依赖链,多个 Frame 是属于一场游戏(Game)的,这个时候,输出应该由 Frame 变成 GameGame 对象构建了 Frame 并把他们串连起来,所以,我们注意力开始再次变化:

Throws 分数 ——> Frame 轮数 ——> Game 一场游戏

现在我们将注意力放到 Game 上面,写一个同样的测试用例。

  • 测试用例

    @Test void testOneThrows() { Game game = new Game(); game.add(5); assertEquals(5, game.score()); }
  • 编译通过

    public class Game { private int itsScore = 0; public int score(){ return 0; } public void add(int pins) { } }
  • 测试通过

    public class Game { private int itsScore = 0; public int score(){ return itsScore; } public void add(int pins) { itsScore += pins; } }

它具有和 Frame 具有同样的功能。但我们任然需要解决以及寻找需要多个 Frame 的证据,因为他是我们使用 Game 的最初理由。我们逐步完成 Game,S 提出编写一个有两次投掷但是没有补中的测试。

  • 测试用例

    @Test void testTwoThrowsNoMark(){ Game game = new Game(); game.add(5); game.add(4); assertEquals(9, game.score()); }
  • 无需修改其他,编译通过

  • 无需修改其他,测试通过

一轮两次的投掷,是没有问题,那么如果两轮四次呢?并且我们需要知道每一轮之后的分数是多少,接下来我们继续测试用例的书写。

  • 测试用例

    @Test void testFourThrowsNoMark(){ Game game = new Game(); game.add(5); game.add(4); game.add(7); game.add(2); assertEquals(18, game.score()); assertEquals(9, game.scoreForFrame(1)); assertEquals(18, game.scoreForFrame(2)); }
  • 编译通过

    public int scoreForFrame(int frame) { return 0; }
  • 测试通过

    public class Game { /** * 投掷序列,最大可能的投掷次数是 21 次 —— C 的回答 */ private int[] itsThrows = new int[21]; /** * 当前第几轮投掷 */ private int itsCurrentThrow = 0; private int itsScore = 0; public int score(){ return itsScore; } public void add(int pins) { // 存放到 投掷序列 中 itsThrows[itsCurrentThrow++] = pins; itsScore += pins; } public int scoreForFrame(int frame) { // 到指定轮数的总分 int score = 0; for (int ball = 0; frame > 0 && (ball < theFrame); ball += 2, frame --) { score += itsThrows[ball] + itsThrows[ball + 1]; } return score; } }

现在测试用例已经通过了,但是 S 提出他似乎不是那么美观,因为他违反了 单一职责原则(SRP),所以需要重构,不过我们暂且把重构这件事情放放,C 来简化这个循环。

public int scoreForFrame(int theFrame) { // 到指定轮数的总分 int score = 0; int ball = 0; for (int currentFrame = 0; currentFrame < theFrame; currentFrame ++) { score += itsThrows[ball++] + itsThrows[ball++]; } return score; }

这样看上去比上面的好了很多,但是 C 觉得会不会有其他问题呢?是的,他可能存在的的问题就是运算符的优先级问题,对于 score 的值似乎和我们预想的不一样。我们稍微修改一下。

public int scoreForFrame(int theFrame) { // 到指定轮数的总分 int score = 0; int ball = 0; for (int currentFrame = 0; currentFrame < theFrame; currentFrame ++) { int firstThrow = itsThrows[ball++]; int secondThrow = itsThrows[ball++]; score += firstThrow + secondThrow; } return score; }

这样可能就明白了很多,对于密友补中和全中的情况,我们似乎已经完成了,来进行一次完整的测试——运行整个 TestGame 类,他的三个已有的测试方法都会是绿色通过。

简单重构

C 提出我们现在的测试似乎有点问题

class TestGame { @Test void testOneGame(){ Game game = new Game(); //... other code } @Test void testTwoThrowsNoMark(){ Game game = new Game(); //... other code } @Test void testFourThrowsNoMark(){ Game game = new Game(); //... other code } }

是的,似乎都是重复性的 new ,十分不友好,也不敏捷,那我们简单的重构下测试吧。

class TestGame { private Game game; // 对于 junit 4 ,你应该使用 @Before 注解 @BeforeEach void setUp() { game = new Game(); } @Test void testOneGame() { game.add(5); assertEquals(5, game.score()); } @Test void testTwoThrowsNoMark(){ game.add(5); game.add(4); assertEquals(9, game.score()); } @Test void testFourThrowsNoMark(){ game.add(5); game.add(4); game.add(7); game.add(2); assertEquals(18, game.score()); assertEquals(9, game.scoreForFrame(1)); assertEquals(18, game.scoreForFrame(2)); } }

修改完后,应该运行整个类,以保证所有的测试方法都是可以通过的。

补中情况

简单重构测试完成,那么我们继续来写关于补中的情况,同样,测试驱动:

  • 测试用例

    @Test void testSimpleSpare(){ game.add(3); game.add(7); game.add(3); assertEquals(13, game.scoreForFrame(1)); }
  • 无需修改,编译通过

  • 测试通过

    public int scoreForFrame(int theFrame) { // 到指定轮数的总分 int score = 0; int ball = 0; for (int currentFrame = 0; currentFrame < theFrame; currentFrame ++) { int firstThrow = itsThrows[ball++]; int secondThrow = itsThrows[ball++]; // 这一轮的分数 int frameScore = firstThrow + secondThrow; // 是否补选 if (frameScore == 10){ // 补选的情况需要加上下一轮的第一次分数 score += frameScore + itsThrows[ball++]; } else { score += frameScore; } } return score; }

C 觉得看起来似乎不错,因为测试用例通过了,但是是否就完成了呢?来进行一个测试

@Test void testSimpleFrameAfterSpare() { game.add(3); game.add(7); game.add(3); game.add(2); assertEquals(13, game.scoreForFrame(1)); assertEquals(18, game.score()); }

结果是红灯,为什么呢?(C 似乎很高兴发现这个错误)看看期望值的得到的值的区别

Expected :18 Actual :15

结果相差三分,就是第三次 投掷 的分数,因为我们在 scoreForFrame 方法最后,使得 ball 加一了,所以跳过了第三次 投掷 的分数,那我们去掉 ++ 看看

if (frameScore == 10){ // 补选的情况需要加上下一轮的第一次分数 score += frameScore + itsThrows[ball++]; }

测试结果

Expected :18 Actual :15

依旧不对且不变,那么我们试着把 game.score() 换成 game.scoreForFrame(2) 试试?

@Test void testSimpleFrameAfterSpare() { game.add(3); game.add(7); game.add(3); game.add(2); assertEquals(13, game.scoreForFrame(1)); assertEquals(18, game.scoreForFrame(2)); }

嘿,他通过了,那么问题应该是出在 score() 方法上了,我们来看看 score 方法:

public int score(){ return itsScore; } public void add(int pins) { // 存放到 投掷序列 中 itsThrows[itsCurrentThrow++] = pins; itsScore += pins; }

C 发现了错误:是的,似乎问题,确实出在这里,因为返回的是 itsScore ,而这个变量标识的仅仅是木瓶数目的综合,但他却不是得分,我们应该让 score 做的是用当前轮作为参数去调用 scoreForFrame() 方法。我们不知道当前哪轮,所以我们需要先写一个能够让我们知道当前第几轮的方法,完善下前面已经通过的所有测试用例:

  • 完善测试用例

    @Test void testOneGame() { game.add(5); assertEquals(5, game.score()); // 当前第一轮 assertEquals(1, game.getCurrentFrame()); } @Test void testTwoThrowsNoMark(){ game.add(5); game.add(4); assertEquals(9, game.score()); // 当前第一轮 assertEquals(1, game.getCurrentFrame()); } @Test void testFourThrowsNoMark(){ game.add(5); game.add(4); game.add(7); game.add(2); assertEquals(18, game.score()); assertEquals(9, game.scoreForFrame(1)); assertEquals(18, game.scoreForFrame(2)); // 当前第二轮 assertEquals(2, game.getCurrentFrame()); }
  • 编译通过

    public int getCurrentFrame() { return 0; }
  • 测试通过

    /** * 当前第几轮 */ private int itsCurrentFrame = 0; /** * 是否是第一次投掷 */ private boolean firstThrow = true; public int getCurrentFrame() { return itsCurrentFrame; } public void add(int pins) { // 存放到 投掷序列 中 itsThrows[itsCurrentThrow++] = pins; itsScore += pins; // 计算当前轮 if (firstThrow){ firstThrow = false; itsCurrentFrame++; } else { firstThrow = true; } }

我们为他添加了两个成员变量用来让我们更好的查找当前轮,然后在 add 里面设置值,这时运行修改的测试用例都是通过了的。不过 add 函数的功能似乎有点多了,我们来把他修改得更易读一些。

public void add(int pins) { // 存放到 投掷序列 中 itsThrows[itsCurrentThrow++] = pins; itsScore += pins; adjustCurrentFrame(); } /** * 计算当前轮 */ private void adjustCurrentFrame() { if (firstThrow){ firstThrow = false; itsCurrentFrame++; } else { firstThrow = true; } }

S 觉得似乎好多了,但是 当前轮 itsCurrentFrame 初始化为 0 ,是不是不太好?因为他不应该初始化为 0 ,应该为 1,游戏是从第一轮开始而不是第 0 轮。并且当前轮应该是正在进行的投掷的所在轮,应该在最后一次投掷完毕,才对他进行递增,而不是第一次投掷就递增,所以修改一下。

  • 修改测试用例

    @Test void testTwoThrowsNoMark(){ game.add(5); game.add(4); assertEquals(9, game.score()); // 第一轮已经结束,到了第二轮了 assertEquals(2, game.getCurrentFrame()); } @Test void testFourThrowsNoMark(){ game.add(5); game.add(4); game.add(7); game.add(2); assertEquals(18, game.score()); assertEquals(9, game.scoreForFrame(1)); assertEquals(18, game.scoreForFrame(2)); // 第二轮已经结束,到了第三轮了 assertEquals(3, game.getCurrentFrame()); }
  • 无需修改,编译通过

  • 测试通过

    private int itsCurrentFrame = 1; /** * 计算当前轮 */ private void adjustCurrentFrame() { if (firstThrow){ firstThrow = false; } else { firstThrow = true; itsCurrentFrame++; } }

C 觉得不错,修改了后,更容易让人理解了。现在我们来为 getCurrentFrame 方法编写两个具有补中情况的测试用例。

@Test void testSimpleSpare(){ game.add(3); game.add(7); game.add(3); assertEquals(13, game.scoreForFrame(1)); assertEquals(2, game.getCurrentFrame()); } @Test void testSimpleFrameAfterSpare() { game.add(3); game.add(7); game.add(3); game.add(2); assertEquals(13, game.scoreForFrame(1)); assertEquals(18, game.scoreForFrame(2)); assertEquals(3, game.getCurrentFrame()); // assertEquals(18, game.score()); }

通过了,现在我们回到原来的 score 的问题上来,现在已经有了 当前轮,那么我们可以大胆的调用 scoreForFrame 方法了:

  • 测试用例

    @Test void testSimpleFrameAfterSpare() { game.add(3); game.add(7); game.add(3); game.add(2); assertEquals(13, game.scoreForFrame(1)); assertEquals(18, game.scoreForFrame(2)); assertEquals(3, game.getCurrentFrame()); assertEquals(18, game.score()); }
  • 无需修改,编译通过

  • 测试通过

    public int score(){ return scoreForFrame(getCurrentFrame() - 1); }

是的,这个方法测试通过了,但是其他的方法呢?在运行整个 测试类,testOneGame 似乎有点问题:

Expected :5 Actual :0
@Test void testOneGame() { game.add(5); assertEquals(5, game.score()); // 当前第一轮 assertEquals(1, game.getCurrentFrame()); }

是代码的问题吗?不,你会发现这个测试用例根本不符合保龄球的规则,所以这个测试用例是不合法的。所以大可以将他直接去掉。

补中的情况就完成了。

全中情况

我们依旧来编写一个全中的测试用例

  • 测试用例

    @Test void test(){ game.add(10); game.add(3); game.add(6); assertEquals(19, game.scoreForFrame(1)); assertEquals(28, game.score()); assertEquals(3, game.getCurrentFrame()); }
  • 无需修改,编译通过

  • 测试通过

    /** * 投掷 * * @param pins 得分 */ public void add(int pins) { // 存放到 投掷序列 中 itsThrows[itsCurrentThrow++] = pins; adjustCurrentFrame(pins); } /** * 计算当前轮 */ private void adjustCurrentFrame(int pins) { if (firstThrow){ if (pins == 10){ // 全中 itsCurrentFrame++; } else { firstThrow = false; } } else { firstThrow = true; itsCurrentFrame++; } } /** * 指定轮的总分 * * @param theFrame 轮 * @return 总分 */ public int scoreForFrame(int theFrame) { // 到指定轮数的总分 int score = 0; int ball = 0; for (int currentFrame = 0; currentFrame < theFrame; currentFrame ++) { int firstThrow = itsThrows[ball++]; if (firstThrow == 10){ // 全中 score += 10 + itsThrows[ball] + itsThrows[ball + 1]; } else { int secondThrow = itsThrows[ball++]; // 这一轮的分数 int frameScore = firstThrow + secondThrow; // 是否补选 if (frameScore == 10){ // 补选的情况需要加上下一轮的第一次分数 score += frameScore + itsThrows[ball]; } else { score += frameScore; } } } return score; }

通过啦!全中的情况似乎完成了?C 提出我们来一次完美的比赛评分看看

@Test void testPerfectGame() { for (int i = 0; i < 12; i++) { game.add(10); } assertEquals(300, game.score()); assertEquals(10, game.getCurrentFrame()); }

但是似乎结果与我们相信的不同

Expected :300 Actual :330

S 一眼就看出来了,是的,当前轮一直被累加到了 12,所以我们应该将他限定在 10,修改一下方法

private void adjustCurrentFrame(int pins) { if (firstThrow){ if (pins == 10){ // 全中 itsCurrentFrame++; } else { firstThrow = false; } } else { firstThrow = true; itsCurrentFrame++; } itsCurrentFrame = Math.min(10, itsCurrentFrame); }

但是。。。C 很暴躁的发现结果似乎不对,因为代码似乎是没有问题的

Expected :300 Actual :270

S 细心的发现, score 需要减一,所以他只给出了第九轮的得分,而不是第十轮,所以因该是十一

private void adjustCurrentFrame(int pins) { if (firstThrow){ if (pins == 10){ // 全中 itsCurrentFrame++; } else { firstThrow = false; } } else { firstThrow = true; itsCurrentFrame++; } itsCurrentFrame = Math.min(11, itsCurrentFrame); }

但是运行后,发现当前轮似乎不对。。。

Expected :10 Actual :11

C 和 S 讨论了一下,似乎觉得这也应该是正确的结果及时有点不舒服(What?)所以应该修改的是测试用例:

@Test void testPerfectGame() { for (int i = 0; i < 12; i++) { game.add(10); } assertEquals(300, game.score()); assertEquals(11, game.getCurrentFrame()); }

S 又想到了一种情况,如果最后数组全满了呢?

@Test void testEndOfArray() { for (int i = 0; i < 9; i++) { game.add(0); game.add(0); } game.add(2); game.add(8); game.add(10); assertEquals(20, game.score()); }

很好,S 很开心因为他也通过了。

再来测试下如果记分板的所有数据输入到程序中呢?

@Test void testSampleGame() { game.add(1); game.add(4); game.add(4); game.add(5); game.add(6); game.add(4); game.add(5); game.add(5); game.add(10); game.add(0); game.add(1); game.add(7); game.add(3); game.add(6); game.add(4); game.add(10); game.add(2); game.add(8); game.add(6); assertEquals(133, game.score()); }

通过啦,C 提议再来测试一下边界情况

@Test void testHeartBreak(){ for (int i = 0; i < 11; i++) { game.add(10); } game.add(9); assertEquals(299, game.score()); }

通过啦,C 再次提议第十轮补中的情况如何:

@Test void testTenthFrameSpare() { for (int i = 0; i < 9; i++) { game.add(10); } game.add(9); game.add(1); game.add(1); assertEquals(270, game.score()); }

重构

C 和 S 都想不出其他的测试用例了,他们觉得应该重构这个这个程序。在这之前,应该测试一下整个 TestGame 测试类的所有方法,保证他们都能够通过。

请注意,重构过程中一定保证所有测试用例都是通过的。

下面来看看第一个需要重构的 scoreForFrame 方法

public int scoreForFrame(int theFrame) { // 到指定轮数的总分 int score = 0; int ball = 0; for (int currentFrame = 0; currentFrame < theFrame; currentFrame ++) { int firstThrow = itsThrows[ball++]; if (firstThrow == 10){ // 全中 score += 10 + itsThrows[ball] + itsThrows[ball + 1]; } else { int secondThrow = itsThrows[ball++]; // 这一轮的分数 int frameScore = firstThrow + secondThrow; // 是否补选 if (frameScore == 10){ // 补选的情况需要加上下一轮的第一次分数 score += frameScore + itsThrows[ball]; } else { score += frameScore; } } } return score; }

emmmmm,,,的确很乱。C 提议可以把 else 下的一堆都给抽离为一个方法,S 提议把局部变量变成成员变量,S 抢过键盘,进行重构。

/** * 是否是第一次投掷 */ private boolean firstThrowInFrame = true; /** * 当前序列 */ private int ball; /** * 第一次投掷 */ private int firstThrow; /** * 第二次投掷 */ private int secondThrow; /** * 计算当前轮 */ private void adjustCurrentFrame(int pins) { if (firstThrowInFrame){ if (pins == 10){ // 全中 itsCurrentFrame++; } else { firstThrowInFrame = false; } } else { firstThrowInFrame = true; itsCurrentFrame++; } itsCurrentFrame = Math.min(11, itsCurrentFrame); } /** * 指定轮的总分 * * @param theFrame 轮 * @return 总分 */ public int scoreForFrame(int theFrame) { // 到指定轮数的总分 int score = 0; ball = 0; for (int currentFrame = 0; currentFrame < theFrame; currentFrame ++) { firstThrow = itsThrows[ball++]; if (firstThrow == 10){ // 全中 score += 10 + itsThrows[ball] + itsThrows[ball + 1]; } else { score += handleSecondThrow(); } } return score; } /** * 第二次投掷的结果 * * @return 分数 */ private int handleSecondThrow(){ int score = 0; secondThrow = itsThrows[ball++]; // 这一轮的分数 int frameScore = firstThrow + secondThrow; // 是否补选 if (frameScore == 10){ // 补选的情况需要加上下一轮的第一次分数 score += frameScore + itsThrows[ball]; } else { score += frameScore; } return score; }

这似乎好多了,**修改完成后,一定要记得运行所有的测试用例保证通过。**但是对于 scoreForFrame 似乎不是那么易理解,C 提出的伪代码

if strike score += 10 + nextTwoBalls(); else if spare score += 10 + nextBall(); else score += twoBallInFrame();

S 看到很高兴,因为这不就是保龄球的积分规则吗?我们改改看,并且去掉 firstThrowsecondThrow 两个成员变量,并用恰当的函数来替代他。

/** * 指定轮的总分 * * @param theFrame 轮 * @return 总分 */ public int scoreForFrame(int theFrame) { // 到指定轮数的总分 int score = 0; ball = 0; for (int currentFrame = 0; currentFrame < theFrame; currentFrame ++) { if (strike()){ ball ++; // 全中 score += 10 + nextTwoBalls(); } else { score += handleSecondThrow(); } } return score; } /** * 第二次投掷的结果 * * @return 分数 */ private int handleSecondThrow(){ int score = 0; // 是否补中 if (spare()){ // 补中的情况需要加上下一轮的第一次分数 ball += 2; score += 10 + nextBall(); } else { score += twoBallsInFrame(); ball += 2; } return score; } /** * 2. 添加方法:是否全中 * * @return 结果 */ private boolean strike() { return itsThrows[ball] == 10; } /** * 3. 添加方法:下面两次投掷的结果之和 * * @return 和 */ private int nextTwoBalls(){ return itsThrows[ball] + itsThrows[ball + 1]; } /** * 4. 添加方法,是否补中 * * @return 补中 */ private boolean spare() { return (itsThrows[ball] + itsThrows[ball + 1]) == 10; } /** * 5. 添加方法:下一次投掷分数 * * @return 分数 */ private int nextBall() { return itsThrows[ball]; } /** * 6. 一轮中的两个投掷结果之和 * * @return 和 */ private int twoBallsInFrame() { return itsThrows[ball] + itsThrows[ball + 1]; }

运行测试用例全部通过,并且不会再有 firstThrowsecondThrowframeScore 三个成员变量了。接下来我们看看,C 提出唯一耦合的就是 ball 这个变量了,现在都是独立处理三种情况的,那我们合并处理看看

/** * 指定轮的总分 * * @param theFrame 轮 * @return 总分 */ public int scoreForFrame(int theFrame) { // 到指定轮数的总分 int score = 0; ball = 0; for (int currentFrame = 0; currentFrame < theFrame; currentFrame ++) { if (strike()){ // 全中 score += 10 + nextTwoBalls(); ball ++; } else if (spare()){ // 补中 score += 10 + nextBallForSpare(); ball += 2; } else { score += handleSecondThrow(); } } return score; } /** * 一轮中的两个投掷结果之和 * * @return 和 */ private int twoBallsInFrame() { return itsThrows[ball] + itsThrows[ball + 1]; }

这样就很帮棒了,一眼就看出来规则。不过 C 和 S 又吵起来了。有一句话非常好:

自上而下,测试优先设计,坦白地说,我不知道这是不是一个好的规则,只是这次,他帮了我们。所以下次,我会再次尝试看看他会发生什么。

他们最后商定将他们分成几个对象,一些小规模的更改。

public class Game { /** * 当前第几轮 */ private int itsCurrentFrame = 1; /** * 分数 */ private int itsScore = 0; /** * 得分运动员 */ private Scorer itsScorer = new Scorer(); /** * 是否是第一次投掷 */ private boolean firstThrowInFrame = true; /** * 计算总分 * * @return 总分 */ public int score(){ return itsScorer.scoreForFrame(getCurrentFrame() - 1); } /** * 投掷 * * @param pins 得分 */ public void add(int pins) { // 存放到 投掷序列 中 itsScorer.addThrow(pins); itsScore += pins; adjustCurrentFrame(pins); } /** * 计算当前轮 */ private void adjustCurrentFrame(int pins) { if (firstThrowInFrame){ if (pins == 10){ // 全中 itsCurrentFrame++; } else { firstThrowInFrame = false; } } else { firstThrowInFrame = true; itsCurrentFrame++; } itsCurrentFrame = Math.min(11, itsCurrentFrame); } /** * 当前第几轮 * * @return 当前轮 */ public int getCurrentFrame() { return itsCurrentFrame; } public int scoreForFrame(int theFrame) { return itsScorer.scoreForFrame(theFrame); } }
public class Scorer { /** * 当前序列 */ private int ball; /** * 投掷序列,最大可能的投掷次数是 21 次 */ private int[] itsThrows = new int[21]; /** * 当前第几轮投掷 */ private int itsCurrentThrow = 0; public void addThrow(int pins) { itsThrows[itsCurrentThrow++] = pins; } /** * 指定轮的总分 * * @param theFrame 轮 * @return 总分 */ public int scoreForFrame(int theFrame) { // 到指定轮数的总分 int score = 0; ball = 0; for (int currentFrame = 0; currentFrame < theFrame; currentFrame ++) { if (strike()){ // 全中 score += 10 + nextTwoBalls(); ball ++; } else if (spare()){ // 补中 score += 10 + nextBallForSpare(); ball += 2; } else { score += handleSecondThrow(); } } return score; } /** * 第二次投掷的结果 * * @return 分数 */ private int handleSecondThrow(){ int score = 0; // 是否补中 if (spare()){ // 补中的情况需要加上下一轮的第一次分数 ball += 2; score += 10 + nextBallForSpare(); } else { score += twoBallsInFrame(); ball += 2; } return score; } /** * 2. 添加方法:是否全中 * * @return 结果 */ private boolean strike() { return itsThrows[ball] == 10; } /** * 3. 添加方法:下面两次投掷的结果之和 * * @return 和 */ private int nextTwoBalls(){ return itsThrows[ball + 1] + itsThrows[ball + 2]; } /** * 4. 添加方法,是否补中 * * @return 补中 */ private boolean spare() { return (itsThrows[ball] + itsThrows[ball + 1]) == 10; } /** * 5. 添加方法:下一次投掷分数 * * @return 分数 */ private int nextBallForSpare() { return itsThrows[ball + 2]; } /** * 6. 一轮中的两个投掷结果之和 * * @return 和 */ private int twoBallsInFrame() { return itsThrows[ball] + itsThrows[ball + 1]; } }

S 很高兴,因为现在 Game 只知晓 FrameScorer 只计算得分,完全符合单一职责原则。

C 发现多余的变量 itsScore

public void add(int pins) { itsScorer.addThrow(pins); adjustCurrentFrame(pins); }

现在应该来看看 adjustCurrentFrame

/** * 计算当前轮 */ private void adjustCurrentFrame(int pins) { if (firstThrowInFrame){ if (pins == 10){ // 全中 itsCurrentFrame++; } else { firstThrowInFrame = false; } } else { firstThrowInFrame = true; itsCurrentFrame++; } itsCurrentFrame = Math.min(11, itsCurrentFrame); }

C 非常不喜欢那个 十一 ,但是却没有办法。。。

private void adjustCurrentFrame(int pins) { if (firstThrowInFrame){ if (pins == 10){ advanceFrame(); } else { firstThrowInFrame = false; } } else { firstThrowInFrame = true; advanceFrame(); } } private void advanceFrame() { itsCurrentFrame = Math.min(11, itsCurrentFrame + 1); }

接下来我们把关于全中的情况判断取出来作为一个独立的方法。

private void adjustCurrentFrame(int pins) { if (firstThrowInFrame){ if(!adjustFrameForStrike(pins)){ firstThrowInFrame = false; } } else { firstThrowInFrame = true; advanceFrame(); } } private boolean adjustFrameForStrike(int pins) { if (pins == 10){ advanceFrame(); return true; } return false; }

接下来,去掉 getCurrentFrame 方法,也去掉调用的地方,就可以把 11 改成 10 啦。

/** * 计算总分 * * @return 总分 */ public int score(){ return itsScorer.scoreForFrame(itsCurrentFrame); } private void advanceFrame() { itsCurrentFrame = Math.min(10, itsCurrentFrame + 1); }

adjustCurrentFrame 似乎有点表意不明

private void adjustCurrentFrame(int pins) { if (!firstThrowInFrame || pins == 10){ advanceFrame(); } else { firstThrowInFrame = false; } }

让他表意更加明确

private void adjustCurrentFrame(int pins) { if (lastBallInFrame(pins)){ advanceFrame(); } else { firstThrowInFrame = false; } } private boolean lastBallInFrame(int pins) { return strike(pins) || !firstThrowInFrame; } private boolean strike(int pins) { return firstThrowInFrame && pins == 10; }

C 和 S 很高兴,因为终于完成了。我也很高兴,因为终于看懂了 T T,所以实践真的很重要。完整的测试(添加显示的名称)

总结

理论结合实践,是学习的不变真理。上周学习的时候不过是一些理论的东西,这周参与实践了一番,不得不说的是,敏捷开发真的挺累,但是效率与结果都让人满意,不过这不就是他诱人的地方吗?其中比较出名的 XP 编程,对于结对的思想也有了概念,但是有时候在想,倘若两个人的思想、基础都存在太大差异,对于弱势方自然收益匪浅,但是对于强势方就是有点累了。不过相比起一个团队的和谐程度,以及进步水平都是具有十分快速的提高的。就像敏捷开发里面的思想:你大可以选择你完全没有接触过和你不懂的专业领域,因为你相信在那里会有人和你一起结对,你可以在这个团队中快速的进步,这就是敏捷开发,一个自组织团队应该有的。

在这次实践中,从一开始两个人的互相思考,再到各自的思想结合,C 总能发现一些小细节,S 总能提出一些很好的解决问题的办法,他们两个能够想到覆盖后面可能出现的情况,从开始设计,到编程,两个人都进行了互补。而我也从这个过程彻底明白了测试驱动开发,在我总结下来三个非常重要的步骤

  1. 编写测试用例
  2. 编译通过
  3. 测试通过

并且,他们对用户素材非常清楚,在最后的重构中,一直不断往用户素材靠拢,例如,他们一直记得保龄球的三种情况,随后重构出来的几个方法的语句是完全和保龄球的规则是对应的。并且每一个语句都能够见名知意,即使有些变量封装成了方法,但是也是一眼就知道什么意思了。他们两总能在彼此看不到的地方提出新的建议,结对莫过于此,忽然感觉,自己一个人学了那么久,变得了自私了很多,这是可悲的,有时候一个人久了,就不太想和别人一起了,以前学习的时候找过别人,但都没有人陪我走下去,最后剩下的也只有自己。这可能是我非常喜欢 XP 思想结对编程的原因之一吧,因为自己十分羡慕这么一份团队。所以也慢慢反省这两年来的大学生活,在余下的大学生活里面也会慢慢改变自己。

后面的章节,学习到了很多重构的细节,但是重构真的是一门学问,始终不太清楚重构到一种什么程度才算完美,可能就是不断地不断地让代码更加易读更加友好,这或许就是重构的意义。重构最后思考来或许可以从以下几点入手:

  1. 代码易读性,能够见名知意。
  2. 尽量消除成员变量,因为永远不知道多少个地方进行修改了,能够选择函数最好。
  3. 一个函数最好负责已经事情,不要让他负责过多的事情。
  4. 多对条件语句的条件进行封装,能够增加代码的易读性。
  5. 尽量遵循一些必要的原则,例如 开放封闭原则、单一职责原则等。
  6. 对于耦合的变量,尽量消除。
  7. 每一次重构,务必要保证所有已有的测试用例通过,才算成功一半。
  8. 有意义的重构,才算成功的另一半。

而作者在结论最后一章提到了几个很重要的点。

  1. 面向对象不是必须的,某些时候,敏捷开发也提倡简单。
  2. 图示有时是不需要的,在创建了他们而没有验证他们的代码就打算遵循他们时,图示就是无意的。
  3. 有时,最好的设计是在你首先编写测试,一小步一小步前进时逐渐形成的。

下一章开始敏捷设计,期待到来。晚安各位 ~

  • 阅读
    89 引用 • 267 回帖 • 4 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...