3.保龄球游戏
- 本系列文章为
laracasts.com
的系列视频教程——Code Katas in PHP 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频,支持正版。- Kata 是一个简短,可重复的编程挑战,可以帮助我们进行快速地编程练习。
- 开发模型仍旧是 TDD(测试驱动开发),视频中使用的是 phpspec 进行开发,笔记中使用了 Laravel 应用,因此代码有不同。
本节说明
- 对应第 3 小节:Bowling Game
本节内容
这一节我们的练习是保龄球游戏。首先你需要花几分钟简单了解下 保龄球的规则,然后我们开始进行练习:
$ php artisan make:test BowlingGameTest --unit
然后新建第一个测试:20 轮空投则分数为 0,如下:
tests\Unit\BowlingGameTest.php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\BowlingGame;
class BowlingGameTest extends TestCase
{
public function setUp()
{
parent::setUp();
$this->bowlingGame = new BowlingGame();
}
/** @test */
public function it_scores_a_gutter_game_as_zero()
{
for($i=0;$i < 20;$i++)
{
$this->bowlingGame->roll(0);
}
$this->assertEquals(0,$this->bowlingGame->score());
}
}
注:
roll()
函数的参数表示被击倒的瓶子数,如roll(0)
表示击倒了 0 个瓶子。
然后运行测试:
注:我们在命令行使用
alias pb="phpunit --filter BowlingGame"
命令设置了别名
我们来让测试通过:
app\BowlingGame.php
<?php
namespace App;
class BowlingGame
{
public function roll($roll)
{
}
public function score()
{
return 0;
}
}
再次测试:
现在可以新建第二个测试:统计所有被击倒的瓶子的分数。我们的测试思路是:如果每一次都只击倒了一个瓶子,那么会有 20 次roll(1)
,且总分为 20。如下:
.
.
/** @test */
public function it_scores_the_sum_0f_all_knocked_down_pins_for_a_game()
{
for($i=0;$i < 20;$i++)
{
$this->bowlingGame->roll(1);
}
$this->assertEquals(20,$this->bowlingGame->score());
}
}
运行测试会失败:
我们需要修改我们的代码:
app\BowlingGame.php
<?php
namespace App;
class BowlingGame
{
protected $score = 0;
public function roll($roll)
{
$this->score += $roll;
}
public function score()
{
return $this->score;
}
}
我们简单地每次加上被击倒的瓶子数作为总得分,然后运行测试:
接下来我们要来测试 补中,即Spare
。假设我们第一轮完成了Spare
,然后剩下的掷球回合击倒数分别为 5 和 17 次 0 击倒,所以总分为:(10 + 5)+ 5 = 20。如下:
.
.
/** @test */
public function it_awards_a_roll_bonus_for_every_spare()
{
$this->bowlingGame->roll(2);
$this->bowlingGame->roll(8); // get a spare
$this->bowlingGame->roll(5);
for($i=0;$i < 17;$i++)
{
$this->bowlingGame->roll(0);
}
$this->assertEquals(20,$this->bowlingGame->score());
}
}
运行测试会失败:
现在我们需要修改代码了,然而,现在你会发现,实际上我们使用roll()
方法来得到总分数,score()
方法并没有起到计算总分的作用。我们应该用roll()
来记录每一次掷球击倒瓶子的数量,而不是来计算得分。那么我们就需要对代码进行重构。当你要进行重构的时候,记住,你的测试必须全部通过。所以我们先注释掉新添加的测试,然后进行重构。
app\BowlingGame.php
<?php
namespace App;
class BowlingGame
{
protected $rolls = [];
public function roll($pints)
{
$this->rolls[] += $pints;
}
public function score()
{
return array_sum($this->rolls);
}
}
运行测试:
我们知道的是,一局游戏分为 10 轮,每轮 2 次掷球(通常状况下),所以接下来我们继续重构,引入轮次的概念:
<?php
namespace App;
class BowlingGame
{
protected $rolls = [];
public function roll($pints)
{
$this->rolls[] += $pints;
}
public function score()
{
$score = 0;
$roll = 0;
for($frame = 1;$frame <= 10;$frame++)
{
$score += $this->rolls[$roll] + $this->rolls[$roll+1];
$roll += 2;
}
return $score;
}
}
每轮我们处理 2 次掷球的得分来计算得到总分数,现在我们来运行测试:
现在重构已经完成了,我们把注释掉的测试恢复,然后进行下一步地开发:
app\BowlingGame.php
.
.
public function score()
{
$score = 0;
$roll = 0;
for($frame = 1;$frame <= 10;$frame++)
{
if($this->rolls[$roll] + $this->rolls[$roll+1] == 10){
$score += 10;
$score += $this->rolls[$roll+2];
}else{
$score += $this->rolls[$roll] + $this->rolls[$roll+1];
}
$roll += 2;
}
return $score;
}
}
然后我们运行测试:
我们的测试通过了,但是我们仍然有工作要做,因为我们现在的代码的可读性很差,如果不借助注释,其他人很难明白代码的逻辑。所以我们需要进行些重构:
<?php
namespace App;
class BowlingGame
{
protected $rolls = [];
public function roll($pints)
{
$this->rolls[] += $pints;
}
public function score()
{
$score = 0;
$roll = 0;
for($frame = 1;$frame <= 10;$frame++)
{
if($this->isSpare($roll)){
$score += 10;
$score += $this->rolls[$roll+2];
}else{
$score += $this->getDefaultFrameScore($roll);
}
$roll += 2;
}
return $score;
}
private function isSpare($roll)
{
return $this->rolls[$roll] + $this->rolls[$roll+1] == 10;
}
private function getDefaultFrameScore($roll)
{
return $this->rolls[$roll] + $this->rolls[$roll+1];
}
}
对比$this->rolls[$roll] + $this->rolls[$roll+1] == 10
,$this->isSpare($roll)
更能告诉阅读代码的人,所需的判断条件是什么。我们来运行测试:
一旦测试通过,我们就可以推进我们的开发工作。但是,我们之前也说过,测试通过也是我们重构代码的好时机。我们来对我们的测试进行些重构,以便更具可读性。我们至少有两个地方可以进行改善:
- 多次执行的
for
循环代码,我们可以封装到一个方法中; - 获得一次
spare
的代码,我们需要做到不借助注释就能让人知道我们获得了一次spare
;
我们进行如下重构:
tests\Unit\BowlingGameTest.php
<?php
namespace Tests\Unit;
use App\BowlingGame;
use Tests\TestCase;
class BowlingGameTest extends TestCase
{
public function setUp()
{
parent::setUp();
$this->bowlingGame = new BowlingGame();
}
/** @test */
public function it_scores_a_gutter_game_as_zero()
{
$this->rollTimes(20,0);
$this->assertEquals(0,$this->bowlingGame->score());
}
/** @test */
public function it_scores_the_sum_0f_all_knocked_down_pins_for_a_game()
{
$this->rollTimes(20,1);
$this->assertEquals(20,$this->bowlingGame->score());
}
/** @test */
public function it_awards_a_roll_bonus_for_every_spare()
{
$this->rollSpare();
$this->bowlingGame->roll(5);
$this->rollTimes(17,0);
$this->assertEquals(20,$this->bowlingGame->score());
}
private function rollSpare()
{
$this->bowlingGame->roll(2);
$this->bowlingGame->roll(8);
}
private function rollTimes($times,$pints)
{
for($i=0;$i < $times;$i++)
{
$this->bowlingGame->roll($pints);
}
}
}
再次运行测试:
现在我们的代码变得很清晰简介,我们可以进行下一个测试:测试 全中,即Strike
。
.
.
/** @test */
public function it_awards_a__two_roll_bonus_for_every_strike()
{
$this->bowlingGame->roll(10);
$this->bowlingGame->roll(7);
$this->bowlingGame->roll(2);
$this->rollTimes(17,0);
$this->assertEquals(28,$this->bowlingGame->score());
}
private function rollSpare()
{
$this->bowlingGame->roll(2);
$this->bowlingGame->roll(8);
}
private function rollTimes($times,$pints)
{
for($i=0;$i < $times;$i++)
{
$this->bowlingGame->roll($pints);
}
}
}
我们第一次掷球获得了strike
,那么该轮结束;第 2 轮我们两次掷球击倒的瓶子数分别为 7 和 2,那么我们的总分为:(10 + 7 + 2) +(7 + 2)= 28。现在我们运行测试会失败:
我们来让测试通过:
.
.
public function score()
{
$score = 0;
$roll = 0;
for($frame = 1;$frame <= 10;$frame++)
{
if($this->rolls[$roll] == 10){
$score += 10 + $this->rolls[$roll+1] + $this->rolls[$roll+2];
}elseif($this->isSpare($roll)){
$score += 10 + $this->rolls[$roll+2];
}else{
$score += $this->getDefaultFrameScore($roll);
}
$roll += 2;
}
.
.
运行测试:
仍然失败,这是因为我们每轮都让roll += 2
,即默认每轮都进行了 2 次掷球。但是对于strike
,我们每轮只掷球一次。所以我们要进行修改:
.
.
public function score()
{
$score = 0;
$roll = 0;
for($frame = 1;$frame <= 10;$frame++)
{
if($this->rolls[$roll] == 10){
$score += 10 + $this->rolls[$roll+1] + $this->rolls[$roll+2];
$roll += 1;
}elseif($this->isSpare($roll)){
$score += 10 + $this->rolls[$roll+2];
$roll += 2;
}else{
$score += $this->getDefaultFrameScore($roll);
$roll += 2;
}
}
return $score;
}
.
.
再次测试:
现在,正如之前我们所做的,重构代码使其更具可读性:
<?php
namespace App;
class BowlingGame
{
protected $rolls = [];
public function roll($pints)
{
$this->rolls[] += $pints;
}
public function score()
{
$score = 0;
$roll = 0;
for($frame = 1;$frame <= 10;$frame++)
{
if($this->isStrike($roll)){
$score += 10 + $this->getStrikeBonus($roll);
$roll += 1;
}elseif($this->isSpare($roll)){
$score += 10 + $this->getSpareBonus($roll);
$roll += 2;
}else{
$score += $this->getDefaultFrameScore($roll);
$roll += 2;
}
}
return $score;
}
private function isSpare($roll)
{
return $this->rolls[$roll] + $this->rolls[$roll+1] == 10;
}
private function isStrike($roll)
{
return $this->rolls[$roll] == 10;
}
private function getDefaultFrameScore($roll)
{
return $this->rolls[$roll] + $this->rolls[$roll+1];
}
private function getStrikeBonus($roll)
{
return $this->rolls[$roll+1] + $this->rolls[$roll+2];
}
private function getSpareBonus($roll)
{
return $this->rolls[$roll+2];
}
}
然后运行测试:
最后让我们添加最后一个测试:满分局。这种情况的出现表明你连续获得了 10 次strike
,所以最终会得到 300 分:
tests\Unit\BowlingGameTest.php
.
.
/** @test */
public function it_scores_a_perfect_game()
{
$this->rollTimes(12,10);
$this->assertEquals(300,$this->bowlingGame->score());
}
.
.
运行测试:
现在我们测练习暂时告一段落,但是也许你还想继续做些工作,例如:roll()
的参数必须是正数之类的防护性工作,那就开始吧!