《敏捷软件开发:原则、模式与实践》笔记(2)
读书  ·  
第六章 一次编程实践
原文保龄球规则:(文末)
https://www.twblogs.net/a/5b957acb2b717750bda47bd5/zh-cn/
原文需求:
记录一届保龄球联赛的所有比赛,确定团队等级,确定每次周赛优胜者和失败者,每场比赛成绩
初步分析数据结构:
- 计分数据
record {
uint32 id primary auto_increase,
uint8 round0_0,
uint8 round0_1,
uint8 round1_0,
uint8 round1_1,
...
uint8 round9_0,
uint8 round9_1,
uint8 round10_0,
uint8 round10_1,
uint8 round10_2,
}
不存储最终该轮得分,该轮得分由函数提供计算,防止冗余数据和出现数据冲突。
- 团队数据
team {
uint32 id primary auto_increase,
string name,
string religin,
uint32 level
}
- 比赛数据
match {
uint32 id primary auto_increase,
uint64 time,
uint32 team_a,
uint32 team_b,
uint32 record_a_id,
uint32 record_b_id,
uint32 winner_id,
}
winner_id 稍微考虑了一下 2 队比赛和多队比赛的可能性(不熟悉规则),以及后期搜索数据的效率。所以不使用 bool 类型。
通过比赛 id 可以构建比赛的三角形淘汰图。
疑问:是否需要计算中的轮数的分值?为了用户体验,默认需要。
初步分析代码:
public class Score {
public static final int ROUNDS = 10;
public static final int FULL_HITS = 10;
public static final int TEN_ROUNDS_THROWS = 20;
public static final int TOTAL_THROWS = TEN_ROUNDS_THROWS + 1;
// 10 轮计分
private int[] scores = new int[ROUNDS];
// 如果是全中轮,则第二轮直接赋值 0,将特殊情况普通化。第十轮可能扔 3 次,所以一共 21 次。
private int[] throws = new int[TOTAL_THROWS];
public void currentRound = 0;
public void currentThrowIndex = 0;
// 用于友好标记不再变化的分数
public void determinedScoreRound = -1;
public void throw(int hits) {
if (!isPlaying()) {
throw new IllegalStateException("it is ended");
}
if (hits < 0 || hits > FULL_HITS) {
throw new IllegalStateException("illegal throws score");
}
boolean isRoundEnd = updateThrowsAndRounds();
if (isRoundEnd) {
updateScores();
}
}
private boolean updateThrowsAndRounds() {
throws[currentThrowIndex++] = throws;
if (throws == FULL_HITS) {
if (isAllFullHits() || isAllOneShot()) {
if (currentThrowIndex == TOTAL_THROWS) {
currentRound++;
return true;
}
} else {
throws[currentThrowIndex++] = 0;
currentRound++;
return true;
}
}
return false;
}
private void updateScores() {
if (isOneShot(beforeLastRound)) {
final int calculateShots = 2;
}
while (int i = determinedScoreRound + 1; i < currentRound; i++) {
if (updateScore(i)) {
determinedScoreRound = i;
}
}
}
/**
* @return boolean is the score determined
**/
private boolean updateScore(int round) {
int score = throws[round * 2] + throws[round * 2 + 1];
if (round == 0) {
scores[round] = score;
return true;
}
int lastRound = round - 1;
score += scores[lastRound];
boolean lastRoundDetermined = determinedScoreRound >= lastRound;
int calculateShots = 0;
boolean needDeteminedRound = round;
if (isOneShot(round)) {
int calculateShots = 2;
needDeteminedRound = round + 2;
} else if (isFullHits(round) {
int calculateShots = 1;
needDeteminedRound = round + 1;
}
int nextRound = round + 1;
while (calculateShots > 0 && nextRound < currentRound) {
score += throws[nextRound * 2];
calculateShots--;
if (isOneShot(nextRound)) {
nextRound++;
continue;
}
score += throws[nextRound * 2 + 1];
calculateShots--;
nextRound++;
}
scores[round] = score;
return lastRoundDetermined && calculateShots = 0;
}
public void isPlaying() {
return currentRound < ROUNDS;
}
public void getRounds() {
return currentRound + 1;
}
private boolean isOneShot(round) {
return throws[round * 2] == FULL_HITS;
}
private boolean isFullHits(round) {
return throws[round * 2] + throws[round * 2 + 1] == FULL_HITS;
}
// 10 轮补中
private boolean isAllFullHits() {
if (currentThrowIndex < TEN_ROUNDS_THROWS) {
return false;
}
for (int i = 0; i < currentRound; i++) {
if (!isFullHits(i)) {
return false;
}
}
return true;
}
// 10 轮全中
private boolean isAllOneShot() {
if (currentThrowIndex < TEN_ROUNDS_THROWS - 1) {
return false;
}
for (int i = 0; i < currentRound; i++) {
if (!isOneShot(i)) {
return false;
}
}
return true;
}
}
阅读原文
做出思考后开始看文章。
首先发现文章一开始提出了 Frame 和 Throw 的概念,而我的代码跳跃性的直接用 int 和 int[] 作为表示。尽管文中也讨论了是否需要这两个对象,但我觉得确实对象化确实是应对复杂软件的良好解决办法。
到了文章中部,他们也用到了 21 和 currentThrow,currentFrame 这两几概念,但很快被质疑了,因为他们不易理解。而不易理解意味着难读懂,更意味着程序容易出错。
同样文中的 scoreForFrame 和我的 updateScore 功能相似。但他们一开始就想到这样设计,因为他们是测试驱动的,或者说是使用用例驱动的。而我是在编写的最后发现原有办法(每次 throw 更新几个 round 的值)难以编写才想出来的。
文中没有 scores 数组,取值由函数代替。这符合尽量简单的原则,依照他们的思路,确实也不需要这个。我现在觉得我这个 scores 数组也非常累赘。
文中先考虑一般情况,再考虑特殊情况,这也是正确的。我在实现一般情况的时候总是会想特殊情况,并将其兼容,这样不利于一个正常流程的实现。
文中的程序性能较差,因为每次获取分数都要从 0 算起,但也减少了很多没必要的变量,例如我的 determinedScoreRound。再说,这程序需要考虑性能吗?
文中代码再持续不断的被重构。每次增加新功能和修改代码,都会重新跑一次测试用例。这非常舒服。
文中目前貌似没有处理全中和补中要投多一次的情况?测试用例只覆盖了分数,没有轮数。
不太赞同为了独立 handleSecondThrow 把好几个局部变量变成全局变量。不过后面的重构也优化了一些,也许先移出去简化结构也是一种好的办法。但 ball 这个临时状态变量还是存在。
ball 也被移到一个计算分数的类 Scorer 去了。
文中最后否定了 Frame 和 Throw 这两个类,增加了 Scorer 类。文中倡导从 Game 开始设计,即自上而下设计。
文中通过限制轮数最大为 11 来处理多投一次的情况,超过 11 轮还是等于 11 轮。是否允许多投 1 或 2 次取决于输入(裁判)。
文中提到,大意:增加各种类来提高软件通用性不等于易于维护(需求变更),易于理解才时易于维护的。
版权所有,转载请注明出处:
https://sickworm.com/?p=1683