Проект «Статистика дрифта». Часть 2. Базовые сущности

от автора

Первая часть серии — Проект «Статистика дрифта». Часть 1. Настройка
Паблик во ВКонтакте с новыми сериями без задержек выпуска на habr — Пихта DEV

Сущность машины

Машина — «рабочая лошадка» пилота дрифта. Давайте уделим ей внимание и сформируем сущность. Что должно быть у машины как сущности? Раз она представляет нам интерес, то как минимум идентификатор. Интерес? Да, это то, что мы будем считать важным для нас. А что такое важно? Это то, что участвует в бизнес логике (например, поиске). А то, что важно, имеет идентификатор. Поэтому, наша машина будет иметь идентификатор, наименование (модель и марка) и параметры двигателя. Почему мы не разделяем наименование на марку и модель? Пока точно не знаю, кажется, что сейчас это не очень актуально. Возможно, в будущем будем разделять для статистики, но пока нет.

Теперь, давайте реализуем нашу сущность, но перед ее написанием нам надо реализовать ValueObject-ы для наших «запчастей» сущности «машина». Создаем директории:

- Domain     - Car         - Entity         - ValueObject             - Engine

Все наши ValueObject будут похожи друг на друга, так что не удивляйтесь. Для начала сделаем VO для идентификатора и наименования машины.

ValueObject для машины

Domain\Car\ValueObject\CardId.php:

<?php  declare(strict_types=1);  namespace Domain\Car\ValueObject;  final readonly class CarId {      private function __construct(private int $value)     {     }      public static function get(int $value): self     {         return new self($value);     }      public static function getNull(): self     {         return new self(0);     }      public function isNull(): bool     {         return $this->value === 0;     }      public function equals(self $id): bool     {         return $this->value === $id->getValue();     }      public function getValue(): int     {         return $this->value;     }  }

Что мы тут используем? По факту мы будем создавать все через статику. Зачем? Как по мне, так проще будет работать с экземплярами, которые создают не снаружи, а внутри класса. Тем самым, мы можем закрыть логику создания объектов. Как видно, get(int $value) создает экземпляр класса с переданным значением, а getNull() делает то же самое, что и get(int $value), но «зашивает» внутри себя логику создания объекта «без значения»/»пустого» ну и прочие синонимы. Ну и дополнительно делаем 2 проверки: isNull(), которая проверяет объект «без значения»/»пустого», и equals(self $id), которая принимаем такой же объект, чтобы сравнить их значения на совпадение.

Domain\Car\ValueObject\CardName.php:

<?php  declare(strict_types=1);  namespace Domain\Car\ValueObject;  final readonly class CarName {      private function __construct(private string $value)     {     }      public static function get(string $value): self     {         return new self($value);     }      public static function getNull(): self     {         return new self('');     }      public function isNull(): bool     {         return $this->value === '';     }      public function equals(self $name): bool     {         return $this->value === $name->getValue();     }      public function getValue(): string     {         return $this->value;     }  }

Думаю, что класс имени машины не требует детального пояснения, так как он аналогичен классу идентификатора машины. Теперь, давайте создадим VO по двигателю. Почему VO, а не Entity? Как мне кажется, двигатель не представляет индивидуального интереса, как это делает машина. И так, что мы тут будем делать: наименование и мощность двигателя. Но, как мне кажется, их лучше будет объединить в VO «двигатель» для удобства использования.

Domain\Car\ValueObject\Engine\EngineName.php:

<?php  declare(strict_types=1);  namespace Domain\Car\ValueObject\Engine;  final readonly class EngineName {      private function __construct(private string $value)     {     }      public static function get(string $value): self     {         return new self($value);     }      public static function getNull(): self     {         return new self('');     }      public function isNull(): bool     {         return $this->value === '';     }      public function equals(self $name): bool     {         return $this->value === $name->getValue();     }      public function getValue(): string     {         return $this->value;     }  }

Что-то знакомое да? Ну конечно, так как такие вещи представляют по всему проекту +- одно и тоже.

Domain\Car\ValueObject\Engine\EnginePower.php:

<?php  declare(strict_types=1);  namespace Domain\Car\ValueObject\Engine;  final readonly class EnginePower {      private function __construct(private int $value)     {     }      public static function get(int $value): self     {         return new self($value);     }      public static function getNull(): self     {         return new self(0);     }      public function isNull(): bool     {         return $this->value === 0;     }      public function equals(self $power): bool     {         return $this->value === $power->getValue();     }      public function getValue(): int     {         return $this->value;     }  }

Тут вообще промолчу… А то каждый раз объяснять все — будет «крайне полезно». А теперь, объединим все в одну VO:

Domain\Car\ValueObject\Engine\Engine.php:

<?php  declare(strict_types=1);  namespace Domain\Car\ValueObject\Engine;  final readonly class Engine {      private function __construct(         private EngineName  $name,         private EnginePower $power     )     {     }      public static function get(EngineName $name, EnginePower $power): self     {         return new self($name, $power);     }      public static function getNull(): self     {         return new self(EngineName::getNull(), EnginePower::getNull());     }      public function isNull(): bool     {         return $this->name->isNull() || $this->power->isNull();     }      public function getName(): EngineName     {         return $this->name;     }      public function getPower(): EnginePower     {         return $this->power;     }  }

Вот, тут немного уточнений. Что мы считаем «отсутствием» двигателя? По-хорошему, когда его нет в машине, но в коде — это отсутствие наименования или мощности. Да, может быть это немного некорректно, но пока пойдет.

Теперь перейдем к созданию сущности машины.

Entity машины

Для начала нам надо определить интерфейс сущности. Для чего? А потому что мы будем иметь как реальную сущность машины, так и сущность «отсутствия» машины (аля nullable object).

Domain\Car\Entity\ICarEntity.php:

<?php  declare(strict_types=1);  namespace Domain\Car\Entity;  use Domain\Car\ValueObject\CarId; use Domain\Car\ValueObject\CarName; use Domain\Car\ValueObject\Engine\Engine;  interface ICarEntity {      public function getName(): CarName;     public function getEngine(): Engine;     public function getId(): CarId;     public function isNull(): bool;  }

Пояснения нужны? Думаю, что нет, то на всякий случай один раз объясню. В интерфейсе есть получение VO: наименования, двигателя и идентификатора машины. Для чего isNull()? А вот это как раз для проверки: «является ли объект машины Nullable Object?», то есть ее «отсутствие».

Теперь давайте реализуем сущность «отсутствия» машины:

Domain\Car\Entity\NullCarEntity.php:

<?php  declare(strict_types=1);  namespace Domain\Car\Entity;  use Domain\Car\ValueObject\CarId; use Domain\Car\ValueObject\CarName; use Domain\Car\ValueObject\Engine\Engine;  final readonly class NullCarEntity implements ICarEntity {      public function getName(): CarName     {         return CarName::getNull();     }      public function getEngine(): Engine     {         return Engine::getNull();     }      public function getId(): CarId     {         return CarId::getNull();     }      public function isNull(): bool     {         return true;     }  }

Просто же? Ну вот и я так думаю. А теперь давайте реализуем сущность машины.

Domain\Car\Entity\CarEntity.php:

<?php  declare(strict_types=1);  namespace Domain\Car\Entity;  use Domain\Car\ValueObject\CarId; use Domain\Car\ValueObject\CarName; use Domain\Car\ValueObject\Engine\Engine;  final readonly class CarEntity implements ICarEntity {      public function __construct(         private CarId   $id,         private CarName $name,         private Engine  $engine,     )     {     }      public function getName(): CarName     {         return $this->name;     }      public function getEngine(): Engine     {         return $this->engine;     }      public function getId(): CarId     {         return $this->id;     }      public function isNull(): bool     {         return false;     }  }

Как видите, здесь мы через конструктор класса передаем нужные VO для создания, а потом просто возвращаем их при получении.

С машиной мы закончили. Теперь надо машину кому-то дать в пользование. А кому? Гонщику, конечно. А начнем мы с небольшого введения.

Сущность гонщика

Тут немного разгуляемся. У гонщика будут: идентификатор, полное имя, номер (под которым выступает), дата рождения, страна, город и машина. Для чего так много? Мне кажется, что такой набор может как-то удачно лечь в более расширенную статистку как программную, так и интеллектуальную (мозгом). Например, мне было бы интересно сравнить двух пилотов, а еще узнать их возраст, чтобы понять, что молодые могут или нет… Да, это странно, но на самом деле даже разница в возрасте может стать интересным предметов аналитики.

Создаем директории:

- Domain     - Racer         - Entity         - ValueObject

Начнем. Сразу скажу, я не буду останавливаться на неинтересных моментах. Так что, читайте внимательно и вникайте.

ValueObject гонщика

Domain\Racer\ValueObject\RacerId.php:

<?php  declare(strict_types=1);  namespace Domain\Racer\ValueObject;  final readonly class RacerId {      private function __construct(private int $value)     {     }      public static function get(int $value): self     {         return new self($value);     }      public static function getNull(): self     {         return new self(0);     }      public function isNull(): bool     {         return $this->value === 0;     }      public function equals(self $id): bool     {         return $this->value === $id->getValue();     }      public function getValue(): int     {         return $this->value;     }  }

Domain\Racer\ValueObject\RacerNumber.php:

<?php  declare(strict_types=1);  namespace Domain\Racer\ValueObject;  final readonly class RacerNumber {      private function __construct(private int $value)     {     }      public static function get(int $value): self     {         return new self($value);     }      public static function getNull(): self     {         return new self(0);     }      public function isNull(): bool     {         return $this->value === 0;     }      public function equals(self $value): bool     {         return $this->value === $value->getValue();     }      public function getValue(): int     {         return $this->value;     }  }

Domain\Racer\ValueObject\RacerCity.php:

<?php  declare(strict_types=1);  namespace Domain\Racer\ValueObject;  final readonly class RacerCity {      private function __construct(private string $value)     {     }      public static function get(string $value): self     {         return new self($value);     }      public static function getNull(): self     {         return new self('');     }      public function isNull(): bool     {         return $this->value === '';     }      public function equals(self $value): bool     {         return $this->value === $value->getValue();     }      public function getValue(): string     {         return $this->value;     }  }

Domain\Racer\ValueObject\RacerCountry.php:

<?php  declare(strict_types=1);  namespace Domain\Racer\ValueObject;  final readonly class RacerCountry {      private function __construct(private string $value)     {     }      public static function get(string $value): self     {         return new self($value);     }      public static function getNull(): self     {         return new self('');     }      public function isNull(): bool     {         return $this->value === '';     }      public function equals(self $value): bool     {         return $this->value === $value->getValue();     }      public function getValue(): string     {         return $this->value;     }  }

А теперь к интересному. Начнем с полного имени.

Domain\Racer\ValueObject\RacerFullName.php:

<?php  declare(strict_types=1);  namespace Domain\Racer\ValueObject;  final readonly class RacerFullName {      private function __construct(         private string $first_name,          private string $last_name,          private string $patronymic     )     {     }      public static function get(string $first_name, string $last_name, string $patronymic): self     {         return new self($first_name, $last_name, $patronymic);     }      public static function getNull(): self     {         return new self('', '', '');     }      public function isNull(): bool     {         return "{$this->first_name}{$this->last_name}{$this->patronymic}" === '';     }      public function getFirstName(): string     {         return $this->first_name;     }      public function getLastName(): string     {         return $this->last_name;     }      public function getValue(): string     {         return $this->last_name . ' ' . $this->first_name . ' ' . $this->patronymic;     }      public function getPatronymic(): string     {         return $this->patronymic;     }      public function equals(self $full_name): bool     {         return $this->getValue() === $full_name->getValue();     }  }

Мы на вход получаем фамилию, имя и отчество. В получении значения getValue() мы конкатенируем это через пробел. В методе сравнения equals() мы просто сравниваем значения. Тут вот есть нюанс… По факту, будет сравнение трех пробелов с тремя пробелами, если сравнить два null объекта. В принципе, ничего страшного, но и не так хорошо.

А теперь переходим в дате рождения. Тут почти то же самое, но есть небольшие отличия — тип данных.

Domain\Racer\ValueObject\RacerDateOfBirth.php:

<?php  declare(strict_types=1);  namespace Domain\Racer\ValueObject;  final readonly class RacerDateOfBirth {      private function __construct(         private int $day,          private int $month,          private int $year     )     {     }      public static function get(int $day, int $month, int $year): self     {         return new self($day, $month, $year);     }      public static function getNull(): self     {         return new self(0, 0, 0);     }      public function isNull(): bool     {         return $this->day + $this->month + $this->year === 0;     }      public function getDay(): int     {         return $this->day;     }      public function getMonth(): int     {         return $this->month;     }      public function getYear(): int     {         return $this->year;     }      public function equals(self $date_of_birth): bool     {         $current = "{$this->day}{$this->month}{$this->year}";         $to_find = "{$date_of_birth->day}{$date_of_birth->month}{$date_of_birth->year}";          return $current === $to_find;     }  }

Вооот… Тут нет getValue(). Почему? А потому что нет смысла возвращать значение даты рождения без применения форматирования. А форматирование даты можно оставить тут, но лучше это использовать вне VO, как мне кажется. Хотя, с другой стороны, мы можем заложить стандартный формат даты, а потом когда-нибудь сделаем форматирование. А давайте!

public function getValue(): string {     return $this->year . '-' . $this->month . '-' . $this->day; }

Как видели, мы в методе equals(self $date_of_birth) сделали странное сравнение. Взяли цифры, превратили в строку, а потом сравнили. Что??? Да, если суммировать их, то можно попасть на комбинацию данных, когда разные даты будут выдавать одно и то же число. Поэтому, было сделано так. Но, мы тут же решили добавить getValue()… Тогда, сравнение equals(self $date_of_birth) теперь можно написать так:

public function equals(self $date_of_birth): bool {     return $this->getValue() === $date_of_birth->getValue(); }

Куда проще и логичней. Все, наконец-то закончили с VO и можем переходить к сущностям.

Entity гонщика

Начнем как обычно с интерфейса, где опишем аналогичным образом методы.

Domain\Racer\Entity\IRacerEntity.php:

<?php  declare(strict_types=1);  namespace Domain\Racer\Entity;  use Domain\Car\Entity\ICarEntity; use Domain\Racer\ValueObject\RacerCity; use Domain\Racer\ValueObject\RacerCountry; use Domain\Racer\ValueObject\RacerDateOfBirth; use Domain\Racer\ValueObject\RacerFullName; use Domain\Racer\ValueObject\RacerId; use Domain\Racer\ValueObject\RacerNumber;  interface IRacerEntity {      public function getId(): RacerId;     public function getFullName(): RacerFullName;     public function getNumber(): RacerNumber;     public function getDateOfBirth(): RacerDateOfBirth;     public function getCounty(): RacerCountry;     public function getCity(): RacerCity;     public function getCar(): ICarEntity;  }

Ну тут все просто, но только отличие от сущности машины в том, что добавляется машина в сущность гонщика. Таким образом, мы связали гонщика и машину. Но, здесь может быть проблема, что у гонщика может быть несколько машин. Например, гонщик меняет машину каждый год. Получается, что когда мы будем как-то записывать статистику, то нам надо учитывать машину в конкретный момент. Когда это надо? Когда мы будем смотреть детальную статистку, то машина нужна, но в общей статистике нет. Поэтому, есть ощущение, что текущая машина гонщика — просто текущая машина. Короче, давайте пока оставим так, а когда дойдем до статистики, возможно, переделаем эту историю с гонщиком. Пока я точно не знаю и уже сам немного запутался. По-хорошему, забегая вперед, нужна какая-то таблица в базе данных (БД), где будет связь гонщика и машины. А ссылка на эту конкретную связь надо указывать в статистике. Тогда мы получим уже более логичный вариант. Да, звучит логично, но на сколько нужен весь список машин гонщика — не знаю. Возможно, чтобы просто вывести эту информацию на личной странице гонщика. Но, в таком случае, нужна еще и дата смены машины, ее «активность» и так далее. Давайте, наверное, пока опустим это свойство и вернемся к нему, когда будем прорабатывать статистику. А там, поверьте, будет оооочень весело.

Domain\Racer\Entity\IRacerEntity.php:

<?php  declare(strict_types=1);  namespace Domain\Racer\Entity;  use Domain\Racer\ValueObject\RacerCity; use Domain\Racer\ValueObject\RacerCountry; use Domain\Racer\ValueObject\RacerDateOfBirth; use Domain\Racer\ValueObject\RacerFullName; use Domain\Racer\ValueObject\RacerId; use Domain\Racer\ValueObject\RacerNumber;  interface IRacerEntity {      public function getId(): RacerId;     public function getFullName(): RacerFullName;     public function getNumber(): RacerNumber;     public function getDateOfBirth(): RacerDateOfBirth;     public function getCounty(): RacerCountry;     public function getCity(): RacerCity;  }

Давайте потом решим, а теперь напишем сущность RacerEntity и NullRacerEntity.

Domain\Racer\Entity\NullRacerEntity.php:

<?php  declare(strict_types=1);  namespace Domain\Racer\Entity;  use Domain\Car\Entity\ICarEntity; use Domain\Car\Entity\NullCarEntity; use Domain\Racer\ValueObject\RacerCity; use Domain\Racer\ValueObject\RacerCountry; use Domain\Racer\ValueObject\RacerDateOfBirth; use Domain\Racer\ValueObject\RacerFullName; use Domain\Racer\ValueObject\RacerId; use Domain\Racer\ValueObject\RacerNumber; use Domain\Team\Entity\ITeamEntity; use Domain\Team\Entity\NullTeamEntity;  final readonly class NullRacerEntity implements IRacerEntity {      public function getId(): RacerId     {         return RacerId::getNull();     }      public function getFullName(): RacerFullName     {         return RacerFullName::getNull();     }      public function getNumber(): RacerNumber     {         return RacerNumber::getNull();     }      public function getDateOfBirth(): RacerDateOfBirth     {         return RacerDateOfBirth::getNull();     }      public function getCounty(): RacerCountry     {         return RacerCountry::getNull();     }      public function getCity(): RacerCity     {         return RacerCity::getNull();     }  }

Domain\Racer\Entity\RacerEntity.php:

<?php  declare(strict_types=1);  namespace Domain\Racer\Entity;  use Domain\Racer\ValueObject\RacerCity; use Domain\Racer\ValueObject\RacerCountry; use Domain\Racer\ValueObject\RacerDateOfBirth; use Domain\Racer\ValueObject\RacerFullName; use Domain\Racer\ValueObject\RacerId; use Domain\Racer\ValueObject\RacerNumber;  final readonly class RacerEntity implements IRacerEntity {      public function __construct(         private RacerId          $id,         private RacerNumber      $number,         private RacerFullName    $full_name,         private RacerDateOfBirth $date_of_birth,         private RacerCountry     $county,         private RacerCity        $city,     )     {     }      public function getId(): RacerId     {         return $this->id;     }      public function getFullName(): RacerFullName     {         return $this->full_name;     }      public function getNumber(): RacerNumber     {         return $this->number;     }      public function getDateOfBirth(): RacerDateOfBirth     {         return $this->date_of_birth;     }      public function getCounty(): RacerCountry     {         return $this->county;     }      public function getCity(): RacerCity     {         return $this->city;     }  }

Вот. На данном моменте мы закончим формирование базовых сущностей для последующей статистики. По факту, получилось на верхнем уровне описать базовые сущности, которые дальше будут фигурировать везде. Остаются только вопросы технической реализации хранения данных для статистики, о которой мы поговорим в другой раз.

Оригинальная статья и новые серии в моем паблике во ВКонтакте Пихта DEV


ссылка на оригинал статьи https://habr.com/ru/articles/842020/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *