В этой серии статей мы строим программное обеспечение марсохода в соответствии со следующими спецификациями. Это позволит применить нам на практике следующие подходы:
- Monolithic Repositories — MonoRepo (Монолитные репозитории)
- Command/Query Responsibility Segregation — CQRS (Сегрегация ответственности на чтение и запись)
- Event Sourcing — ES (События как источник)
- Test Driven Development — TDD (Разработка через тестирование)
Марсоход, Введение
Марсоход, Инициализация
Марсоход, Посадка
Марсоход, Координаты посадки
Ранее мы создали пакет навигации, теперь можно приступать к разработке первого варианта использования:
Марсоход должен будет сначала приземлиться в заданном положении. Положение состоит из координат (X
иY
, являющихся целыми числами) и ориентации (строковое значениеnorth
,east
,west
илиsouth
).
Упрощаем Command Bus (Командная Шина)
Паттерн Command Bus состоит из 3х классов:
- класс
Command
, проверяющий входной набор данных с именем к которому будут применятся необходимые манипуляции (например,LandRover
) - связанный с ним (отношение один к одному)
CommandHandler
, реализующий логику для конкретного случая использования CommandBus
, который принимает команды и выполняет соответствующиеCommandHandler
, также поддерживает работу черезMiddleware
Мы собираемся упростить этот архитектурный шаблон для нашего марсохода, опуская класс CommandBus
, т.к. нам на самом деле не нужно реализовывать middleware
или искать соответствующий CommandHandler
для полученной Command
.
Начнем с создания класса Command
, который позаботится о проверке входных параметров:
cd packages/navigation
git checkout -b 2-landing
Приземление
Мы собираемся инициализировать тестовый класс для LandRover
, используя phpspec:
vendor/bin/phpspec describe 'MarsRover\Navigation\LandRover'
Получается сгенерированный класс spec/MarsRover/Navigation/LandRoverSpec.php
:
namespace spec\MarsRover\Navigation;
use MarsRover\Navigation\LandRover;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class LandRoverSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(LandRover::class);
}
}
Нам остается отредактировать его, начнем с описания входных параметров:
namespace spec\MarsRover\Navigation;
use PhpSpec\ObjectBehavior;
class LandRoverSpec extends ObjectBehavior
{
const X = 23;
const Y = 42;
const ORIENTATION = 'north';
function it_has_x_coordinate()
{
$this->beConstructedWith(
self::X,
self::Y,
self::ORIENTATION
);
$this->getX()->shouldBe(self::X);
}
function it_has_y_coordinate()
{
$this->beConstructedWith(
self::X,
self::Y,
self::ORIENTATION
);
$this->getY()->shouldBe(self::Y);
}
function it_has_an_orientation()
{
$this->beConstructedWith(
self::X,
self::Y,
self::ORIENTATION
);
$this->getOrientation()->shouldBe(self::ORIENTATION);
}
}
Теперь можно запустить тесты:
vendor/bin/phpspec run
Это сгенерирует нам src/MarsRover/Navigation/LandRover.php
файл:
namespace MarsRover\Navigation;
class LandRover
{
private $argument1;
private $argument2;
private $argument3;
public function __construct($argument1, $argument2, $argument3)
{
$this->argument1 = $argument1;
$this->argument2 = $argument2;
$this->argument3 = $argument3;
}
public function getX()
{
}
public function getY()
{
}
public function getOrientation()
{
}
}
Все, что нам нужно сделать, это изменить его:
namespace MarsRover\Navigation;
class LandRover
{
private $x;
private $y;
private $orientation;
public function __construct($x, $y, $orientation)
{
$this->x = $x;
$this->y = $y;
$this->orientation = $orientation;
}
public function getX() : int
{
return $this->x;
}
public function getY() : int
{
return $this->y;
}
public function getOrientation() : string
{
return $this->orientation;
}
}
Давайте снова выполним тесты:
vendor/bin/phpspec run
Все зеленые! Но наша работа еще не закончена, мы не описали недопустимые входные параметры:
namespace spec\MarsRover\Navigation;
use PhpSpec\ObjectBehavior;
class LandRoverSpec extends ObjectBehavior
{
const X = 23;
const Y = 42;
const ORIENTATION = 'north';
function it_has_x_coordinate()
{
$this->beConstructedWith(
self::X,
self::Y,
self::ORIENTATION
);
$this->getX()->shouldBe(self::X);
}
function it_cannot_have_non_integer_x_coordinate()
{
$this->beConstructedWith(
'Nobody expects the Spanish Inquisition!',
self::Y,
self::ORIENTATION
);
$this->shouldThrow(
\InvalidArgumentException::class
)->duringInstantiation();
}
function it_has_y_coordinate()
{
$this->beConstructedWith(
self::X,
self::Y,
self::ORIENTATION
);
$this->getY()->shouldBe(self::Y);
}
function it_cannot_have_non_integer_y_coordinate()
{
$this->beConstructedWith(
self::X,
'No one expects the Spanish Inquisition!',
self::ORIENTATION
);
$this->shouldThrow(
\InvalidArgumentException::class
)->duringInstantiation();
}
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();
}
}
Снова проверяем:
vendor/bin/phpspec run
Они падают, потому что мы должны проверять входные параметры:
namespace MarsRover\Navigation;
class LandRover
{
const VALID_ORIENTATIONS = ['north', 'east', 'west', 'south'];
private $x;
private $y;
private $orientation;
public function __construct($x, $y, $orientation)
{
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;
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 getX() : int
{
return $this->x;
}
public function getY() : int
{
return $this->y;
}
public function getOrientation() : string
{
return $this->orientation;
}
}
И снова выполним тесты:
vendor/bin/phpspec run
Все прошли! Теперь мы можем закоммитить нашу работу:
git add -A
git commit -m '2: Created LandRover'
Заключение
Мы сделали первые шаги в TDD: написали тесты, затем код, и с помощью phpspec
этот процесс упростился.
Поскольку мы пишем эти тесты в описательном виде (тестовые методы именуются в виде предложений), то мы можем использовать их в качестве исполнимой спецификации для самоконтроля! phpspec
позволяет отображать их в явном виде:
vendor/bin/phpspec run --format=pretty
Должно отображаться:
MarsRover\Navigation\LandRover
13 has x coordinate
24 cannot have non integer x coordinate
37 has y coordinate
48 cannot have non integer y coordinate
61 has an orientation
72 cannot have a non cardinal orientation
1 specs
6 examples (6 passed)
10ms
Примечание:navigation
-тесты можно запускать из MonoRepo:
cd ../../ composer update --optimize-autoloader vendor/bin/phpspec run
Что дальше
В следующей статье мы завершим цикл TDD рефакторингом LandRover
: извлечем x
и y
координаты в их собственные классы.
Предыдущая часть: Марсоход, Инициализация
Следующая часть: Марсоход, Координаты посадки