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

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

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

回顾

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

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

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

一次编程实战

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

这一章节,是一次 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. 有时,最好的设计是在你首先编写测试,一小步一小步前进时逐渐形成的。

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

  • 阅读
    85 引用 • 242 回帖 • 4 关注

相关帖子

欢迎来到这里!

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

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