4.字符串计算
- 本系列文章为
laracasts.com
的系列视频教程——Code Katas in PHP 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频,支持正版。- Kata 是一个简短,可重复的编程挑战,可以帮助我们进行快速地编程练习。
- 开发模型仍旧是 TDD(测试驱动开发),视频中使用的是 phpspec 进行开发,笔记中使用了 Laravel 应用,因此代码有不同。
本节说明
- 对应第 4 小节:String Calculator
本节内容
这一节我们的练习是字符串计算。首先我们定义几条规则:
StringCalculator::add('')
的结果是 0;StringCalculator::add('1,2,3')
的结果是 6;- 参数不能包含负数;
StringCalculator::add('1,2,3,1000')
的结果是 6,即过滤超出范围的数字;- 允许用换行符
\n
进行分割;
开始我们的练习:
$ php artisan make:test StringCalculatorTest --unit
然后开始第一个测试:
tests\Unit\StringCalculatorTest.php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\StringCalculator;
class StringCalculatorTest extends TestCase
{
public function setUp()
{
parent::setUp();
$this->stringCalculator = new StringCalculator();
}
/** @test */
public function it_translate_empty_string_into_zero()
{
$this->assertEquals(0,$this->stringCalculator->add(''));
}
}
运行测试:
我们来使测试通过:
app\StringCalculator.php
<?php
namespace App;
class StringCalculator
{
public function add()
{
return 0;
}
}
运行测试:
向前推进:
.
.
/** @test */
public function it_finds_the_sum_of_one_number()
{
$this->assertEquals(5,$this->stringCalculator->add('5'));
}
}
运行测试:
修改代码:
<?php
namespace App;
class StringCalculator
{
public function add($number)
{
return (int) $number;
}
}
再次测试:
继续推进,计算两个数的和:
tests\Unit\StringCalculatorTest.php
.
.
/** @test */
public function it_finds_the_sum_of_one_number()
{
$this->assertEquals(5,$this->stringCalculator->add('5'));
}
/** @test */
public function it_finds_the_sum_of_two_numbers()
{
$this->assertEquals(3,$this->stringCalculator->add('1,2'));
}
}
运行测试会失败:
我们来让测试通过:
<?php
namespace App;
class StringCalculator
{
public function add($numbers)
{
$numbers = explode(',',$numbers);
return array_sum($numbers);
}
}
运行测试:
继续推进:
.
.
/** @test */
public function it_finds_the_sum_of_any_numbers()
{
$this->assertEquals(15,$this->stringCalculator->add('1,2,3,4,5'));
}
}
运行测试:
继续推进,测试参数包含负数时抛出异常:
.
.
/** @test */
public function it_disallows_negative_numbers()
{
$this->expectException('InvalidArgumentException');
$this->stringCalculator->add('1,2,-3,4,5');
}
}
运行测试:
查看我们的代码会发现我们没有地方来进行判断,所以我们要先进行重构。正如我们之前说过的,进行代码重构的时候,必须要保证你的测试全部通过。所以我们注释掉新添加的测试,然后进行如下重构:
<?php
namespace App;
class StringCalculator
{
public function add($numbers)
{
$numbers = explode(',',$numbers);
$solution = 0;
foreach($numbers as $number){
$solution += (int)$number;
}
return $solution;
}
}
运行测试:
然后我们取消注释,在循环中进行判断:
<?php
namespace App;
use InvalidArgumentException;
class StringCalculator
{
public function add($numbers)
{
$numbers = explode(',',$numbers);
$solution = 0;
foreach($numbers as $number){
if($number < 0) throw new InvalidArgumentException;
$solution += (int)$number;
}
return $solution;
}
}
再次运行测试:
继续推进:
.
.
/** @test */
public function it_ignores_any_number_that_is_one_thousand_or_greater()
{
$this->assertEquals(3,$this->stringCalculator->add('1,2,1000'));
}
}
运行测试:
让测试通过:
.
.
public function add($numbers)
{
$numbers = explode(',',$numbers);
$solution = 0;
foreach($numbers as $number){
if($number < 0) throw new InvalidArgumentException;
if($number >= 1000) continue;
$solution += (int)$number;
}
return $solution;
}
}
运行测试:
继续推进,允许使用换行符进行分割:
.
.
/** @test */
public function it_allows_new_line_delimiters()
{
$this->assertEquals(6,$this->stringCalculator->add('1,2\n3'));
}
}
运行测试:
我们加入了一个新的分割符合:\n
,所以explode()
函数不再适用,我们改用正则表达式匹配分割。别忘了,我们在做的事情是重构,所以我们需要保证我们的测试是全部通过的。先注释掉新添加的测试然后再进行修改:
.
.
$numbers = preg_split('/\s*,\s*/',$numbers);
.
.
注:我们同时去除了
,
前后的空格
然后我们来运行之前的全部测试:
现在取消掉注释,再让新的测试通过:
.
.
$numbers = preg_split('/\s*(,|\\\n)\s*/',$numbers);
.
.
运行测试:
好了,现在我们的开发告一段落,但是,我们仍然可以进行重构工作。因为我们的测试是全部通过的,所以我们有相当大地自信来进行重构。首先,当参数包含负数时,我们期望得到具体的信息,如下:
.
.
/** @test */
public function it_disallows_negative_numbers()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('Invalid number provided:-3');
$this->stringCalculator->add('1,2,-3,4,5');
}
.
.
然后修改代码给出异常消息:
.
.
public function add($numbers)
{
$numbers = preg_split('/\s*(,|\\\n)\s*/',$numbers);
$solution = 0;
foreach($numbers as $number){
if($number < 0) throw new InvalidArgumentException("Invalid number provided:{$number}");
if($number >= 1000) continue;
$solution += (int)$number;
}
return $solution;
}
.
.
运行测试:
接着我们对业务代码进行重构:
<?php
namespace App;
use InvalidArgumentException;
class StringCalculator
{
const MAX_NUMBER_ALLOWED = 1000;
public function add($numbers)
{
$numbers = $this->parseNumbers($numbers);
$solution = 0;
foreach($numbers as $number){
$this->guardAgainstInvalidNumber($number);
if($number >= self::MAX_NUMBER_ALLOWED) continue;
$solution += $number;
}
return $solution;
}
private function guardAgainstInvalidNumber($number)
{
if($number < 0) throw new InvalidArgumentException("Invalid number provided:{$number}");
}
private function parseNumbers($numbers)
{
return array_map('intval',preg_split('/\s*(,|\\\n)\s*/',$numbers));
}
}
我们对三个地方进行了重构:
- 字符串分割为数组的逻辑我们抽取到了
parseNumbers()
方法中,并且转换成int
; - 参数为负数的验证逻辑我们抽取到了
guardAgainstInvalidNumber()
方法中; - 规定的最大值我们定义了常量
MAX_NUMBER_ALLOWED
来进行维护;
重构并不复杂,但是可读性大大增强了,不是吗?