Pull to refresh

Марсоход, Посадка

Reading time 5 min
Views 5.9K
Original author: Loïc Faugeron


В этой серии статей мы строим программное обеспечение марсохода в соответствии со следующими спецификациями. Это позволит применить нам на практике следующие подходы:


  • 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 координаты в их собственные классы.


Предыдущая часть: Марсоход, Инициализация
Следующая часть: Марсоход, Координаты посадки

Tags:
Hubs:
+10
Comments 0
Comments Leave a comment

Articles