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


  • 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.


Предыдущая часть: Марсоход, Посадка

Поделиться с друзьями
-->

Комментарии (0)