В этой серии статей мы строим программное обеспечение марсохода в соответствии со следующими спецификациями. Это позволит применить нам на практике следующие подходы:
- Monolithic Repositories — MonoRepo (Монолитные репозитории)
- Command/Query Responsibility Segregation — CQRS (Сегрегация ответственности на чтение и запись)
- Event Sourcing — ES (События как источник)
- Test Driven Development — TDD (Разработка через тестирование)
Марсоход, Введение
Марсоход, Инициализация
Марсоход, Посадка
Марсоход, Координаты посадки
В предыдущих частях мы создали пакет навигации, а в нем LandRover
класс, который валидирует входные параметры для нашего первого способа использования:
Марсоход должен будет сначала приземлиться в заданном положении. Положение состоит из координат (X
иY
, являющихся целыми числами) и ориентации (строковое значениеnorth
,east
,west
илиsouth
).
Сегодня мы будем рефакторить LandRover
:
cd packages/navigation
git checkout 2-landing
Ответственность
Посмотрев на LandRover
, можно найти 2 причины для изменения:
- координаты
x
иy
могут приниматьfloat
значения, или иметь дополнительную осьz
- ориентация может быть в угловых градусах или иметь вертикальную ориентацию.
Это намекает на два новых класса, извлеченных из LandRover
: Coordinates
и Orientation
. В этой статье мы позаботимся о координатах.
Координаты
Сначала сделаем тестовый класс, используя phpspec:
vendor/bin/phpspec describe 'MarsRover\Navigation\Coordinates'
Появится новый файл spec/MarsRover/Navigation/CoordinatesSpec.php
:
namespace spec\MarsRover\Navigation;
use MarsRover\Navigation\Coordinates;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class CoordinatesSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(Coordinates::class);
}
}
Мы отредактируем его, используя наработки из тестового класса для LandRover
:
namespace spec\MarsRover\Navigation;
use PhpSpec\ObjectBehavior;
class CoordinatesSpec extends ObjectBehavior
{
const X = 23;
const Y = 42;
function it_has_x_coordinate()
{
$this->beConstructedWith(
self::X,
self::Y
);
$this->getX()->shouldBe(self::X);
}
function it_cannot_have_non_integer_x_coordinate()
{
$this->beConstructedWith(
'Nobody expects the Spanish Inquisition!',
self::Y
);
$this->shouldThrow(
\InvalidArgumentException::class
)->duringInstantiation();
}
function it_has_y_coordinate()
{
$this->beConstructedWith(
self::X,
self::Y
);
$this->getY()->shouldBe(self::Y);
}
function it_cannot_have_non_integer_y_coordinate()
{
$this->beConstructedWith(
self::X,
'No one expects the Spanish Inquisition!'
);
$this->shouldThrow(
\InvalidArgumentException::class
)->duringInstantiation();
}
}
Если запустить тесты сейчас, будет загружен класс CoordinatesSpec
:
vendor/bin/phpspec run
И он создаст нам файл src/MarsRover/Navigation/Coordinates.php
:
namespace MarsRover\Navigation;
class Coordinates
{
private $argument1;
private $argument2;
public function __construct($argument1, $argument2)
{
$this->argument1 = $argument1;
$this->argument2 = $argument2;
}
public function getX()
{
}
public function getY()
{
}
}
Теперь остается только завершить то, что мы уже делали для класса LandRover
:
namespace MarsRover\Navigation;
class Coordinates
{
private $x;
private $y;
public function __construct($x, $y)
{
if (false === is_int($x)) {
throw new \InvalidArgumentException(
'X coordinate must be an integer'
);
}
$this->x = $x;
if (false === is_int($y)) {
throw new \InvalidArgumentException(
'Y coordinate must be an integer'
);
}
$this->y = $y;
}
public function getX() : int
{
return $this->x;
}
public function getY() : int
{
return $this->y;
}
}
Запустим тесты:
vendor/bin/phpspec run
Все зеленые! Обновим тестовый класс LandRover
для использования в нем нового класса координат:
namespace spec\MarsRover\Navigation;
use PhpSpec\ObjectBehavior;
class LandRoverSpec extends ObjectBehavior
{
const X = 23;
const Y = 42;
const ORIENTATION = 'north';
function it_has_coordinates()
{
$this->beConstructedWith(
self::X,
self::Y,
self::ORIENTATION
);
$coordinates = $this->getCoordinates();
$coordinates->getX()->shouldBe(self::X);
$coordinates->getY()->shouldBe(self::Y);
}
function it_has_an_orientation()
{
$this->beConstructedWith(
self::X,
self::Y,
self::ORIENTATION
);
$this->getOrientation()->shouldBe(self::ORIENTATION);
}
function it_cannot_have_a_non_cardinal_orientation()
{
$this->beConstructedWith(
self::X,
self::Y,
'A hareng!'
);
$this->shouldThrow(
\InvalidArgumentException::class
)->duringInstantiation();
}
}
Больше не нужно валидировать значения x
и y
, все это доверим классу Coordinates
, он позаботится об этом для нас. Теперь можно обновить класс LandRover
:
namespace MarsRover\Navigation;
class LandRover
{
const VALID_ORIENTATIONS = ['north', 'east', 'west', 'south'];
private $coordinates;
private $orientation;
public function __construct($x, $y, $orientation)
{
$this->coordinates = new Coordinates($x, $y);
if (false === in_array($orientation, self::VALID_ORIENTATIONS, true)) {
throw new \InvalidArgumentException(
'Orientation must be one of: '
.implode(', ', self::VALID_ORIENTATIONS)
);
}
$this->orientation = $orientation;
}
public function getCoordinates() : Coordinates
{
return $this->coordinates;
}
public function getOrientation() : string
{
return $this->orientation;
}
}
Еще раз проверим все ли в порядке, запустив тесты:
vendor/bin/phpspec run
Отлично, все прошло! Закоммитим изменения:
git add -A
git commit -m '2: Created Coordinates'
Заключение
Мы прошли полный цикл TDD: тест, код, рефакторинг. Использование phpspec
было очень полезно для прототипирования тестовых классов, а затем и самого кода.
Что дальше
В следующей статье мы выделим Orientation
из LandRover
.
Предыдущая часть: Марсоход, Посадка