Первая часть серии - Проект «Статистика дрифта». Часть 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

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


  1. SerafimArts
    09.09.2024 22:45
    +3

    А директория ValueObject ну прям нужна, да? Выглядит как такая же бессмысленная, как классические Service, Manager, Helper, Support, etc (или суффиксы Entity, выглядит как "масло масляное").

    P.S. Если уж хочется венгерскую нотацию использовать, то в пыхе приняты суффиксы: IRacerEntity -> RacerInterface.

    P.P.S. А ещё вместо PSR-2/12 лучше PER-2 использовать.

    P.P.P.S А ещё не хватает хотя бы кратенькой ремарки в чём прикол анемичных энтитей. Я не говорю что это плохо, сам считаю что анемичные легче и практичнее, но всё же многие считают это плохой практикой и даже антипаттерном.

    P.P.P.P.S. А ещё нулевой даты не существует: RacerDateOfBirth::getNull() возвращает явно некорректное и недопустимое состояние.

    P.P.P.P.P.S. А ещё можно порезать нормально интерфейсы, согласно ISP, например:

    // До
    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\Shared\
    interface ValueObjectInterface 
    {
        public function equals(self $value): bool;
    }
    
    // Domain\Shared\
    interface IdInterface extends ValueObjectInterface {}
    
    // Domain\Shared\
    interface IdentifiableInterface
    {
        public function getId(): IdInterface;
    }
    
    // Domain\Racer\
    interface NameableInterface 
    {
        public function getName(): \Stringable|string;
    }
    
    // Domain\Racer\ (?)
    interface ContainLocationInterface
    {
        public function getCity(): CityInterface; // Город уже содержит внутри страну
    }
    
    // Domain\Car\
    interface InteractWithCarInterface
    {
        public function getCar(): CarInterface;
    }
    
    // Domain\Racer\
    interface RacerInterface extends
        IdentifiableInterface,
        NameableInterface,
        ContainLocationInterface,
        InteractWithCarInterface
    {
        #[\Override]
        public function getId(): RacerId;
        public function getNumber(): RacerNumber;
        public function getDateOfBirth(): RacerDateOfBirth;
    }
    

    А так -- как всегда огонь. Только воды (копипасты) много. Можно было описать пару энетитей, пару VO, а остальное схематично, без приведения кода.


    1. mepihin Автор
      09.09.2024 22:45

      В будущей статье, которая есть в ВК, были проведены работы по рефакторингу. По стандарту PSR можно использовать Interface в конце. А еще выделение Shared моментов в VO.

      А ещё вместо PSR-2/12 лучше PER-2 использовать.

      Чем?

      А ещё не хватает хотя бы кратенькой ремарки в чём прикол анемичных энтитей. Я не говорю что это плохо, сам считаю что анемичные легче и практичнее, но всё же многие считают это плохой практикой и даже антипаттерном.

      В проекте на данный момент не предусматривается изменение, лишь вывод. Исходя из этого, нет необходимости в создании методов изменения сущности

      А ещё нулевой даты не существует: RacerDateOfBirth::getNull() возвращает явно некорректное и недопустимое состояние.

      Этим никто пользоваться не будет, поскольку у нас есть просто факт VO без значений. И эти нули нужны просто для заполнения VO. Процесс взаимодействия будет строиться через isNull, где что-то будет не отображаться и так далее.


      1. SerafimArts
        09.09.2024 22:45
        +2

        Чем?

        Чтоб не писать вот такое:

        public function __construct(
            public string $ololo
        ) // << вот
        { // << это
        } // << вот
        
        // А было адекватно
        public function __construct(
            public string $ololo,
        ) {}
        

        В проекте на данный момент не предусматривается изменение, лишь вывод. Исходя из этого, нет необходимости в создании методов изменения сущности

        Я ничего не говорил про мутабельность, я говорил про бледную модель ;)

        Этим никто пользоваться не будет, поскольку у нас есть просто факт VO без значений.

        В таком случае почему вместо VO не сделать null? Это же не джава, где null является любым объектом и типизация считай что не работает. В пыхе более строгая и накосячить не получится. Так что выглядит как public ?RacerDateOfBirth $birthDate = null, не?

        Ну и проверка или вывод идентичный: $racer?->birth->getDay(). ...хотя я бы вообще сделал $racer?->birth->day, т.к. свойства (в первую очередь) и поля (тут уже спорно) должны отвечать за хранение/отдачу данных, а не методы. Это, типа, их семантика) Методы что-то делают, поля хранят, а свойства предоставляют доступ к полям. В случае же VO у нас ридонли поля, которые смело сделать публичными ридонли.


        1. mepihin Автор
          09.09.2024 22:45

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

          По поводу VO я отвечал в комментариях паблике ВК. Nullable Object позволяет "стабильнее" и однозначней использовать объекты. Дополнительный null тип будет вводить уже меньшую степень "объектности" и двойственности. Поэтому, при подходе максимального использования объектов NULL не вписывается адекватно. Помимо этого в isNull я могу зашить логику "а что такое отсутствие" тем самым "спрятав" это в объект.

          Про свойства. Я считаю, что все свойства должны быть приватными или защищёнными. Публичные свойства доступны только для редактирования из вне. Геттеры и сеттеры - та же логика, что я описал выше про isNull.


          1. SerafimArts
            09.09.2024 22:45
            +1

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

            Ну вот лично я полностью поддерживаю подобное, иначе потом сущности превращаются в монстров и божественные объекты. Мой тезис касался исключительно того, что неплохо было бы это в статье как-то объяснить, а то есть много статей, где это считается антипаттерном, например: https://habr.com/ru/articles/224879/

            По поводу VO я отвечал в комментариях паблике ВК. Nullable Object позволяет "стабильнее" и однозначней использовать объекты.

            Ну тут не паблик ВК, а хабр. Тут подобного нет)

            Помимо этого в isNull я могу зашить логику "а что такое отсутствие" тем самым "спрятав" это в объект.

            А что мешает тоже самое сделать во время вызова?

            final readonly class ExampleName
            {
            
                // ... всякий код
            
                public function isEmpty(): bool
                {
                    return $this->name === '';
                }
            }
            
            // Использование
            public ?ExampleName $name {
                get => $this->name?->isEmpty() ? null : $this->name;
            }
            // Ну или через метод
            public function getName(): ?ExampleName
            {
                return $this->name?->isEmpty() ? null : $this->name;
            }
            

            А по поводу тезиса "не объектный", в принципе согласен, но по-моему удобство в использовании и поддержке кода в данном случае на порядки важнее, чем довольно спорный околофилософский тезис.

            Про свойства. Я считаю, что все свойства должны быть приватными или защищёнными. Публичные свойства доступны только для редактирования из вне. Геттеры и сеттеры - та же логика, что я описал выше про isNull.

            Так геттеры и сеттеры и реализуют свойства. По факту это костыль для языков, где нет свойств. Имхо, при наличии на уровне языка свойств ни одного аргумента в пользу их эмуляции через геттеры/сеттеры нет. А вся эта чушь с гет/сет была притянута в древнее время из джавы, где всё чем они занимались - контролировали non-null, коих проблем в PHP нет.

            Ну и понятно, что все эти аргументы на тему (из популярного):

            • "ну это же инкапсуляция" -- а где инкапсуляция-то (?), добавить приват - это не инкапсуляция.

            • "неизвестен тип" -- не актуально, начиная с 7.4, ранее через @var делалалось.

            • "любой может записать" -- не актуально, начиная с 8.0, ранее через @readonly или @psalm-readonly-allow-private-mutation делалось.

            • "невозможно определить в интерфейсе" -- не актуально, начиная с 8.4, ранее через @property-read костыль делалось.

            • "невозможно добавить логику на запись/чтение" -- не актуально, начиная с 8.4, ранее через мега-костыль __get/__set делалось.

            Из реально более-менее аргументированного последние 2 пункта, да и то в современном PHP уже не актуально (ну кроме тех мест, где ещё php 5.6).

            Отсюда и мои выводы выше, что поля класса можно смело делать публичными только на чтение (readonly), а свойства уже любые. Отсюда и тезисы о getXxx/setXxx методах, что они фактически легаси, которые тащат зачем-то по-привычке, не имея ни одного аргумента в пользу их применения.


            1. SerafimArts
              09.09.2024 22:45

              А по поводу тезиса "не объектный", в принципе согласен, но по-моему удобство в использовании и поддержке кода в данном случае на порядки важнее, чем довольно спорный околофилософский тезис.

              В любом случае, имхо, возвращать заведомо некорректные данные ну или иметь VO в невалидном состоянии хуже, нежели просто там возвращать null (при наличии такой возможности), т.к. не все и не всегда будут проверять при каждом вызове if ($entity->getVo()->isNull()) { ... }, а значит это повышает шанс написать забагованный/некорректный код.


  1. milinsky
    09.09.2024 22:45
    +2

    Я правильно понимаю, "сущности" будут иммутабельными?


    1. mepihin Автор
      09.09.2024 22:45

      Скорее да. В контексте этого проекта сущности, которые работает только на вывод (на данный момент), не будут в ходе бизнес логики меняться.