Как по мне достаточно важный, хотя и холиварный вопрос. Эта статья аккумулирует в себе те практики, которые мне близки и которых я придерживаюсь в разработке.
Статья рассчитана не на новичков, потому нормально, если по ходу чтения какие-то понятия будут вам неизвестны, я постарался коротко раскрыть их здесь, а также указал ссылки на посты в моём телеграм канале Beer:HP , которые могут чуть подробнее раскрыть то или иное понятие.
В следующей части мы рассмотрим и пользовательскую валидацию и поговорим про ограничения в базе данных, но начнем мы сразу с доменного слоя нашего приложения, то есть с той самой бизнес логики.
Как этого добиться?
В конструкторе необходимо проверять, что данные адекватны, например, что значения находятся в допустимом диапазоне, все значения присутствуют и т.д. В случае если что-то не так - вы должны выбрасывать исключения.
class Order
{
private Product $product;
private int $quantity;
public function __construct(Product $product, int $quantity)
{
if ($quantity <= 0) {
throw new MinQuantityException();
}
}
}
Но ведь мы же не будем показывать пользователям исключения?
Всё правильно, исключения не для пользователей. Exceptions, трассировка и контекст должны быть видны только разработчикам. Все исключения выброшенные разработчиком должны быть обработаны перед тем как вывести пользователю что-то на экран.
Например вы работаете с заказами. Заказ товара может быть отменен, если он не доставлен. Вместо того чтобы где-то во вне сущности делать:
$order->getStatus();
// isn't delivered
$order->setCancel();
Определите метод cancel(), который будет выполнять проверки внутри сущности и если всё согласовано — менять её состояние.
class Order
{
// ...
public function cancel(): void
{
if ($this->status === STATUS:ELIVERED) {
throw new LogicException(
sprintf(
'Order %s has already been delivered',
$this->id->asString()
)
);
}
$this->status = STATUS::CANCEL;
}
}
class Account
{
private string $accountNumber;
private float $amount = 0.00;
private string $currency = 'USD';
const NUMBER_OF_CHARACTERS = 16;
public function __construct(string $accountNumber)
{
if (strlen($accountNumber) !== self::NUMBER_OF_CHARACTERS) {
// throw exception
}
$this->accountNumber = $accountNumber;
}
public function putMoney(float $amount, string $currency)
{
if ($amount <= 0) {
// throw exception
}
if ($currency !== $this->currency) {
//thow exception
}
$this->amount += $amount;
}
}
Мы можем отдельно вынести AccountNumber, переместив в него всю валидацию.
Код AccountNumber
Отдельно выделить Value Object Money который также может взять на себя операцию сложения для логики пополнения счета.
Код Money
Тогда наша Entity будет иметь примерно следующий вид:
class Account
{
private AccountNumer $accountNumber;
private Money $money;
public function __construct(AccountNumer $accountNumber)
{
$this->accountNumber = $accountNumber;
$this->money = new Money(0.00, 'USD');
}
public function putMoney(Money $money)
{
$this->money = $this->money->append($money);
}
}
Так как в основной сущности мы уже работаем с валидными Value Objects, то нет необходимости проверять что-то дополнительно внутри сущности, мы и так всё затайпхинтили.
Наш пример (Money). У нас есть сумма денег, которую нам нужно сложить с другой суммой. Чтобы принять решение можем ли мы сложить две amount, мы должны проверить currency. Поскольку currency напрямую влияет на логику вычислений, то оно должно находиться там-же, где и amount.
Это может быть что угодно, такие штуки как валюта, координаты, календарный период, номер телефона, расстояние, вес и т.д.
Eсли у нас есть данные которые влияют на логику - они должны быть частью состояния объекта где эта логика реализована. Да-да, вычисления (логика) также должны находиться внутри (например сложение/вычитание денег или вычисление расстояния в случае с гео).
Если же в объекте хранятся данные которые на логику реализованную в этом объекте никак не влияют - было бы неплохо эти данные оттуда вынести чтобы не мешали.
Это не значит, что нужно совсем перестать оборачивать в VO примитивные типы (строки, числа и т.д.). Это значит, что при проектировании стоит задумываться о целесообразности того или иного объекта в вашей предметной области.
Если в качестве связи с другой сущностью в метод или в конструктор мы передаём ID, то мы наверняка не можем быть уверены, что Entity с таким ID существует в рамках нашей системы, ведь на входе мы можем убедиться лишь в том, что ID соответствует определенному шаблону (например UUID).
В следующей части мы поговорим с вами о пользовательской валидации и подробнее разберём исключения.
Для самых нетерпеливых уже есть короткая заметка в телеграм канале Beer:HP. Подписывайтесь, чтобы получать информацию первыми.
Источник статьи: https://habr.com/ru/post/566394/
Статья рассчитана не на новичков, потому нормально, если по ходу чтения какие-то понятия будут вам неизвестны, я постарался коротко раскрыть их здесь, а также указал ссылки на посты в моём телеграм канале Beer:HP , которые могут чуть подробнее раскрыть то или иное понятие.
В следующей части мы рассмотрим и пользовательскую валидацию и поговорим про ограничения в базе данных, но начнем мы сразу с доменного слоя нашего приложения, то есть с той самой бизнес логики.
Валидация Entity
Рано или поздно, пользовательские данные переданные в наше приложение попадают во внутрь Entity.Но не "просто хранят". Сущность всегда должна защищать свои доменные инварианты и следить за тем, чтобы она находилась в согласованном состоянии.Entity — это объекты, которые хранят состояние вашего приложения.
Entity не должна существовать в вашем приложении если внутри неполные или невалидные данные.Инварианты — это некоторые условия, которые остаются истинными на протяжении всей жизни объекта. Как правило, инварианты передают внутреннее состояние объекта.
Как этого добиться?
Проверка данных в конструкторе
Конструктор должен принимать все параметры, которые обязательны для существования сущности, а также валидировать их перед тем, как присвоить значение свойству. Все необязательные параметры могут быть заданы значениями по-умолчанию или быть присвоенными отдельными методами, в которых также следует добавлять проверки перед присваиванием.В конструкторе необходимо проверять, что данные адекватны, например, что значения находятся в допустимом диапазоне, все значения присутствуют и т.д. В случае если что-то не так - вы должны выбрасывать исключения.
class Order
{
private Product $product;
private int $quantity;
public function __construct(Product $product, int $quantity)
{
if ($quantity <= 0) {
throw new MinQuantityException();
}
}
}
Но ведь мы же не будем показывать пользователям исключения?
Всё правильно, исключения не для пользователей. Exceptions, трассировка и контекст должны быть видны только разработчикам. Все исключения выброшенные разработчиком должны быть обработаны перед тем как вывести пользователю что-то на экран.
Проверка данных в методе
Когда обновление определенного поля фактически представляет действие, выполняемое над объектом, определите для него метод в самой сущности. Задача такого метода также заключается в проверке предоставленных ему данных, он должен убедиться, что можно обновить данные, учитывая текущее состояние объекта.Например вы работаете с заказами. Заказ товара может быть отменен, если он не доставлен. Вместо того чтобы где-то во вне сущности делать:
$order->getStatus();
// isn't delivered
$order->setCancel();
Определите метод cancel(), который будет выполнять проверки внутри сущности и если всё согласовано — менять её состояние.
class Order
{
// ...
public function cancel(): void
{
if ($this->status === STATUS:ELIVERED) {
throw new LogicException(
sprintf(
'Order %s has already been delivered',
$this->id->asString()
)
);
}
$this->status = STATUS::CANCEL;
}
}
Используйте Value Objects для проверки отдельных значений
Данный подход позволяет и делегировать проверки, и переиспользовать их в дальнейшем в других частях нашего приложения. Для примера возьмем класс Account, который уже валидирует свои данные в конструкторе и в одном из методов:class Account
{
private string $accountNumber;
private float $amount = 0.00;
private string $currency = 'USD';
const NUMBER_OF_CHARACTERS = 16;
public function __construct(string $accountNumber)
{
if (strlen($accountNumber) !== self::NUMBER_OF_CHARACTERS) {
// throw exception
}
$this->accountNumber = $accountNumber;
}
public function putMoney(float $amount, string $currency)
{
if ($amount <= 0) {
// throw exception
}
if ($currency !== $this->currency) {
//thow exception
}
$this->amount += $amount;
}
}
Мы можем отдельно вынести AccountNumber, переместив в него всю валидацию.
Код AccountNumber
Отдельно выделить Value Object Money который также может взять на себя операцию сложения для логики пополнения счета.
Код Money
Тогда наша Entity будет иметь примерно следующий вид:
class Account
{
private AccountNumer $accountNumber;
private Money $money;
public function __construct(AccountNumer $accountNumber)
{
$this->accountNumber = $accountNumber;
$this->money = new Money(0.00, 'USD');
}
public function putMoney(Money $money)
{
$this->money = $this->money->append($money);
}
}
Так как в основной сущности мы уже работаем с валидными Value Objects, то нет необходимости проверять что-то дополнительно внутри сущности, мы и так всё затайпхинтили.
Whole value concept (Quantity pattern)
Я часто вижу, что этому концепту уделяют мало внимания при проектировании Value Objects, потому решил отдельно на нём остановиться.Идея простая. Представим, что у нас есть геопозиция. Чтобы понять где именно находится точка нам нужна и широта и долгота. Поскольку сами по себе "широта" или "долгота" не имеют смысла друг без друга, значит они должны находиться в одном месте, внутри одного объекта. Другими словами не нужно создавать отдельные VO, если сами по себе они ничего не значат, а только являются составляющей другого объекта.Следует создавать и использовать объекты, которые имеют значение в рамках вашего бизнеса.
Наш пример (Money). У нас есть сумма денег, которую нам нужно сложить с другой суммой. Чтобы принять решение можем ли мы сложить две amount, мы должны проверить currency. Поскольку currency напрямую влияет на логику вычислений, то оно должно находиться там-же, где и amount.
Это может быть что угодно, такие штуки как валюта, координаты, календарный период, номер телефона, расстояние, вес и т.д.
Eсли у нас есть данные которые влияют на логику - они должны быть частью состояния объекта где эта логика реализована. Да-да, вычисления (логика) также должны находиться внутри (например сложение/вычитание денег или вычисление расстояния в случае с гео).
Если же в объекте хранятся данные которые на логику реализованную в этом объекте никак не влияют - было бы неплохо эти данные оттуда вынести чтобы не мешали.
Это не значит, что нужно совсем перестать оборачивать в VO примитивные типы (строки, числа и т.д.). Это значит, что при проектировании стоит задумываться о целесообразности того или иного объекта в вашей предметной области.
Не нужно создавать для Entity сервисы валидации
В доменном слое это усложнит вам жизнь. Вам придется делать бесконечные и ненужные геттеры внутри Entity (ведь для валидатора данные нужно будет как-то извлечь), следить за тем что нужно обновить сервис в случае изменения самой сущности и не забывать его вызвать каждый раз при её создании.Связь с другой сущностью
Отношения лучше выстраивать с помощью идентификаторов, а не по ссылкам на объект. Таким образом мы понижаем связанность (Low Coupling), а также убираем возможность нежелательных изменений, которые могут происходить внутри связанной сущности.Если в качестве связи с другой сущностью в метод или в конструктор мы передаём ID, то мы наверняка не можем быть уверены, что Entity с таким ID существует в рамках нашей системы, ведь на входе мы можем убедиться лишь в том, что ID соответствует определенному шаблону (например UUID).
Заключение
Правильное проектирование валидации бизнес логики само по себе сильно упростит вам жизнь. Оперируйте в вашем приложении только полными, валидными и консистентными объектами.В следующей части мы поговорим с вами о пользовательской валидации и подробнее разберём исключения.
Для самых нетерпеливых уже есть короткая заметка в телеграм канале Beer:HP. Подписывайтесь, чтобы получать информацию первыми.
Источник статьи: https://habr.com/ru/post/566394/