Понимание Value Objects/Объектов Значения в PHP

Источник: «Understanding Value Objects in PHP»
Value Objects/Объекты Значения — это фантастическая концепция, которую мы можем использовать для улучшения наших приложений. Они представляют собой небольшие объекты, такие как Money, DateRange, Email или Age, которые мы используем в сложных приложениях. Они являются ключевыми элементами при создании эффективного, понятного и сопровождаемого кода.

Объекты Значения характеризуются неизменяемостью и оцениваются по состоянию, а не по идентичности. В отличие от Entity Objects/Объектов Сущности, которые обладают определённой идентичностью, Объекты Значения не обладают каким-либо уникальным идентификатором. Вместо этого они полностью определяются своим значением, т.е. считается, что два Объекта Значений равны, если их значения совпадают, независимо от того, являются ли они отдельными экземплярами.

Например, возьмём два экземпляра Объекта Значения Money, один из которых создан для представления 10$, а другой отдельно инициирован также для представления 10$. Хотя это два разных экземпляра, в рамках приложения мы считаем их равными, поскольку они представляют одно и то же значение.

Объекты Значения делают ваш код более явным, читаемым и менее подверженным ошибкам. Они инкапсулируют связанные данные в логические и осмысленные концепции и контекст, что облегчает управление, тестирование и отладку. Понимание и правильная реализация Объектов Значений могут значительно упростить сложную бизнес-логику и повысить качество кодовой базы.

Зачем нужны Объекты Значения

Они обладают целым рядом преимуществ, которые непосредственно влияют на качество кода и удобство сопровождения приложений. Давайте обсудим некоторые причины, по которым следует интегрировать Объекты Значения в наши приложения, и как это может привести к повышению качества кода.

Во-первых, Объекты Значения приводят к самопроверяемому коду. Поскольку Объекты Значения инкапсулируют определённый фрагмент бизнес-логики, они отвечают за то, чтобы всегда находиться в корректном состоянии. Например, Объект Значения Email не будет создан, если данное электронное письмо не будет правильно отформатировано. Таким образом, код становится менее подверженным ошибкам, поскольку он защищает от недействительных состояний.

Во-вторых, Объекты Значения уменьшают дублирование кода. Любое дублирование в кодовой базе увеличивает её сложность и затрудняет сопровождение. Использование Объектов Значения позволяет избежать повторения кода в кодовой базе. Вместо этого вы инкапсулируете определённое поведение в Объект Значения, который может быть использован в различных частях приложения.

Ещё одним существенным преимуществом использования Объектов Значения является их способность улучшать выразительность и читаемость кода. Объекты Значения объединяют связанные значения в одну осмысленную конструкцию, делая ваш код более понятным для других (и для вас, когда вы пересматриваете свой собственный код спустя месяцы!). Например, объект DateRange, содержащий начальную и конечную даты, гораздо интуитивнее и чище, чем работа с двумя отдельными переменными даты.

Объекты Значения также укрепляют доменную модель приложения, обеспечивая целостность данных на протяжении всего их жизненного цикла. Поскольку Объекты Значения являются неизменяемыми (можно создать их изменяемыми, но лучше сделать их неизменяемыми), они значительно снижают вероятность возникновения несоответствий в данных.

Value Objects (VOs) vs Data Transfer Objects (DTOs)

Некоторые люди могут запутаться, когда мы говорим об Объектах Значения/Value Objects (VOs) и Объектах Передачи Данных/Data Transfer Objects (DTOs), и иногда даже смешивают эти понятия, но они служат разным целям и имеют различные характеристики.

Объекты Значения/Value Objects (VOs) — это объекты, представляющие не сущность, а конкретное значение. Обычно они используются для инкапсуляции отдельного фрагмента данных или комбинации связанных данных, например, даты, цвета или суммы в валюте. Они предназначены для использования в качестве иммутабельных контейнеров данных и часто применяются для обеспечения соблюдения бизнес-правил или валидации данных.

С другой стороны, Объекты Передачи Данных/Data Transfer Objects (DTOs) — это объекты, которые используются для передачи данных между различными уровнями или модулями приложения. Они переносят данные из одной части системы в другую и используются в основном для связи и сериализации, к ним также можно добавить правила валидации, но обычно они не содержат никакой логики. Они часто представляют собой подмножество данных из сущности или нескольких сущностей и могут включать дополнительные поля или преобразования для удовлетворения специфических требований канала связи или клиента.

Начало использования Объектов Значения

Я продемонстрирую, как можно начать использовать Объекты Значения в приложениях, показав на простом примере, как можно инкапсулировать логику работы со значениями Money в приложении.

Представьте себе, что в нашем приложении есть две Сущности/Entities

final class Product
{
public function __construct(
private string $name,
private float $price,
private string $currency,
) {}
}

final class Subscription
{
public function __construct(
private string $name,
private float $price,
private string $currency,
) {}
}

Вы можете видеть, что у нас есть две общие вещи для обеих Сущностей: price и currency, и что оба значения имеют связь между собой. В этом сценарии у нас есть два варианта: мы можем продублировать код для обработки этих значений для обеих сущностей или создать трейт, или какой-либо вспомогательный класс. Это может помочь, но это не лучшее решение. Здесь нам могут помочь Объекты Значения, мы можем создать Объект Значения Money, который мы можем повторно использовать во всей нашей кодовой базе, и в нем будет заключена вся бизнес-логика и весь контекст для работы с денежными значениями.

final readonly class Money
{
private float $amount;
private string $currency;

public function __construct(float $amount, string $currency)
{
$this->ensureAmountIsPositive($amount);

$this->amount = $amount;
$this->currency = $currency;
}

public function getAmount(): float
{
return $this->amount;
}

public function getCurrency(): string
{
return $this->currency;
}

public function add(Money $other): Money
{
$this->assertSameCurrency($other);

return new Money($this->amount + $other->getAmount(), $this->currency);
}

public function subtract(Money $other): Money
{
$this->assertSameCurrency($other);

return new Money($this->amount - $other->getAmount(), $this->currency);
}

public function isSameCurrency(Money $other): bool
{
return $this->currency === $other->currency;
}

public function equals(Money $other): bool
{
return $this->amount === $other->getAmount() && $this->isSameCurrency($other);
}

public function greaterThan(Money $other): bool
{
$this->assertSameCurrency($other);

return $this->amount > $other->getAmount();
}

public function lessThan(Money $other): bool
{
$this->assertSameCurrency($other);

return $this->amount < $other->getAmount();
}

private function assertSameCurrency(Money $other): void
{
if (! $this->isSameCurrency($other)) {
throw new InvalidArgumentException("Currencies should be the same.");
}
}

private function ensureAmountIsPositive(float $amount): void
{
if ($amount < 0) {
throw new InvalidArgumentException("Amount should be positive.");
}
}
}

Как видно из приведённого примера, теперь вся бизнес-логика, необходимая для работы с денежными знаками, упакована в Объект Значения Money, который можно использовать во всех приложениях без необходимости дублирования кода. Мы можем ещё больше усовершенствовать Объект Значения Money, создав ещё один Объект Значения для валют.

final readonly class Currency
{
public function __construct(
private string $code,
private string $name,
) {}

public function getCode(): string
{
return $this->code;
}

public function getName(): string
{
return $this->name;
}

public function equals(Currency $other): bool
{
return $this->code === $other->getCode();
}
}

Затем мы можем обновить Объект Значение Money, чтобы он использовал Объект Значение Currency.

final readonly class Money
{
private float $amount;
private Currency $currency;

public function __construct(float $amount, Currency $currency)
{
$this->ensureAmountIsPositive($amount);

$this->amount = $amount;
$this->currency = $currency;
}

public function getAmount(): float
{
return $this->amount;
}

public function getCurrency(): Currency
{
return $this->currency;
}

public function add(Money $other): Money
{
$this->assertSameCurrency($other);

return new Money($this->amount + $other->getAmount(), $this->currency);
}

public function subtract(Money $other): Money
{
$this->assertSameCurrency($other);

return new Money($this->amount - $other->getAmount(), $this->currency);
}

public function isSameCurrency(Money $other): bool
{
return $this->currency->equals($other->getCurrency());
}

public function equals(Money $other): bool
{
return $this->amount === $other->getAmount() && $this->isSameCurrency($other);
}

public function greaterThan(Money $other): bool
{
$this->assertSameCurrency($other);

return $this->amount > $other->getAmount();
}

public function lessThan(Money $other): bool
{
$this->assertSameCurrency($other);

return $this->amount < $other->getAmount();
}

private function assertSameCurrency(Money $other): void
{
if (! $this->isSameCurrency($other)) {
throw new InvalidArgumentException("Currencies should be the same.");
}
}

private function ensureAmountIsPositive(float $amount): void
{
if ($amount < 0) {
throw new InvalidArgumentException("Amount should be positive.");
}
}
}

Теперь, когда у нас есть готовый к использованию Объект Значение Money, мы можем обновить Сущности Product и Subscription для его использования.

final class Product
{
public function __construct(
private string $name,
private Money $price,
) {}
}

final class Subscription
{
public function __construct(
private string $name,
private Money $price,
) {}
}

Заключение

В заключение следует отметить, что Объекты Значения являются мощным инструментом, который, инкапсулируя связанные значения и поведения в чистые, многократно используемые части, значительно повышает качество кода, читаемость и длительную сопровождаемость приложений.

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

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

Внедряя в приложения Объекты Значения, мы можем разрабатывать приложения, которые не просто созданы для работы, а созданы для развития, созданы для понимания и созданы для долговечности.

Я надеюсь, что Вам понравилась эта статья, и если это так, то не забудьте поделиться этой статьёй со своими друзьями!!! До встречи!

Дополнительные материалы

Предыдущая Статья

Полное руководство по типу Never в TypeScript

Следующая Статья

TypeScript: Сравнение Типа и Интерфейса