快一周过去了,再次拿起这本书。周六是美好的日子,没有学校的课程,不用担心作业,没有人约,也不用去考虑太多其他琐事,只需要静下来看会儿书,多么惬意美好。
回顾
总得回顾一下上周看了些什么的,然而写读后感的好处莫过于不用再去翻书啦。看看自己上周写的文章,文字不多不少,但是也是体会颇深,也会有一些不同的体会。
- 敏捷是有组织的,是团队性的。
- 敏捷开发方法:极限编程。
- 用户素材 与 任务计划。
- 测试驱动开发,从无到有的构建。
- 重构是必要的。
今天开始学习第六章,一次编程实战,长文预警!
一次编程实战
设计和编程都是人的活动,忘记了这一点,将会失去一切。
这一章节,是一次 XP 的编程的实践,章节中采用的是以对话的形式展现,相比于无聊的阐述,以故事的方式却好了很多。汲取朋友的经验,在这之前先去最后一页翻看了 保龄球比赛 规则,了解了规则后再看看确实棒很多!但是发现光看的话不能全身心的投入进去,所以准备实践一番。
在这里,我将我所学习到的分为连个部分来总结,不过在那之前,还是需要了解的应该是保龄球规则~~~
保龄球规则
保龄球是一种比赛,比赛者把一个哈密瓜大小的球顺着一条窄窄的球道投向 10 个木瓶。目的是在每次投球中击倒尽可能多的木瓶。
一局比赛由 10 轮组成。每轮开始,10 个木瓶都是竖立摆放的。比赛者可以投球两次,尝试击倒所有木瓶。
如果比赛者在第一次投球中就击倒了所有木瓶,称之为“全中”,并且本轮结束。
如果比赛者在第一次投球中没有击倒所有木瓶,但在第二次投球中成功击倒了所有剩余的木瓶,称之为“补中”。一轮中第二次投球后,即使还有未被击倒的木瓶,本轮也宣告结束。
- 全中轮的记分规则为:10,加上接下来的两次投球击倒的木瓶数,再加上前一轮的得分。
- 补中轮的记分规则为:10,加上接下来的一次投球击倒的木瓶数,再加上前一轮的得分。
- 其他轮的记分规则为:本轮中两次投球所击倒的木瓶数,加上前一轮的得分。
如果在第 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
变成 Game
。Game
对象构建了 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 看到很高兴,因为这不就是保龄球的积分规则吗?我们改改看,并且去掉 firstThrow
和 secondThrow
两个成员变量,并用恰当的函数来替代他。
/**
* 指定轮的总分
*
* @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];
}
运行测试用例全部通过,并且不会再有 firstThrow
和 secondThrow
和 frameScore
三个成员变量了。接下来我们看看,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
只知晓 Frame
,Scorer
只计算得分,完全符合单一职责原则。
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 总能提出一些很好的解决问题的办法,他们两个能够想到覆盖后面可能出现的情况,从开始设计,到编程,两个人都进行了互补。而我也从这个过程彻底明白了测试驱动开发,在我总结下来三个非常重要的步骤
- 编写测试用例
- 编译通过
- 测试通过
并且,他们对用户素材非常清楚,在最后的重构中,一直不断往用户素材靠拢,例如,他们一直记得保龄球的三种情况,随后重构出来的几个方法的语句是完全和保龄球的规则是对应的。并且每一个语句都能够见名知意,即使有些变量封装成了方法,但是也是一眼就知道什么意思了。他们两总能在彼此看不到的地方提出新的建议,结对莫过于此,忽然感觉,自己一个人学了那么久,变得了自私了很多,这是可悲的,有时候一个人久了,就不太想和别人一起了,以前学习的时候找过别人,但都没有人陪我走下去,最后剩下的也只有自己。这可能是我非常喜欢 XP 思想结对编程的原因之一吧,因为自己十分羡慕这么一份团队。所以也慢慢反省这两年来的大学生活,在余下的大学生活里面也会慢慢改变自己。
后面的章节,学习到了很多重构的细节,但是重构真的是一门学问,始终不太清楚重构到一种什么程度才算完美,可能就是不断地不断地让代码更加易读更加友好,这或许就是重构的意义。重构最后思考来或许可以从以下几点入手:
- 代码易读性,能够见名知意。
- 尽量消除成员变量,因为永远不知道多少个地方进行修改了,能够选择函数最好。
- 一个函数最好负责已经事情,不要让他负责过多的事情。
- 多对条件语句的条件进行封装,能够增加代码的易读性。
- 尽量遵循一些必要的原则,例如 开放封闭原则、单一职责原则等。
- 对于耦合的变量,尽量消除。
- 每一次重构,务必要保证所有已有的测试用例通过,才算成功一半。
- 有意义的重构,才算成功的另一半。
而作者在结论最后一章提到了几个很重要的点。
- 面向对象不是必须的,某些时候,敏捷开发也提倡简单。
- 图示有时是不需要的,在创建了他们而没有验证他们的代码就打算遵循他们时,图示就是无意的。
- 有时,最好的设计是在你首先编写测试,一小步一小步前进时逐渐形成的。
下一章开始敏捷设计,期待到来。晚安各位 ~
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于