В этой серии статей мы строим программное обеспечение марсохода в соответствии со следующими спецификациями. Это позволит применить нам на практике следующие подходы:
- 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.
Предыдущая часть: Марсоход, Посадка

