При моделировании такого понятия предметно-ориентированного проектирования как сущность могут возникнуть некоторые сложности, обусловленные бизнес-требованиями или технической частью. В частности, иногда возникает сложность с созданием объекта-сущности.
В данной статье описываются две такие проблемы, и рассматривается способ их решения. Так же статья подойдет как введение в проектирование сущностей. Для понимания материала понадобится базовое представление о предметно-ориентированном проектировании.
Итак, мы изучили предметную область, сформировали единый язык, выделили ограниченные контексты и определились с требованиями [2]. Всё это выходит за рамки данной статьи, тут мы попробуем решить конкретные узкие проблемы:
- Создание и обеспечение консистентности сложных объектов-сущностей.
- Создание объектов-сущностей с генерацией идентификатора по автоинкрементному полю базы данных.
Введение
У нас есть клиент, который должен быть смоделирован как сущность (Entity) [2]. С точки зрения бизнеса у каждого клиента обязательно есть:
- имя или наименование;
- организационная форма (физ. лицо, ИП, ООО, АО и т.д.);
- главный менеджер (один из менеджеров, закрепляется за клиентом);
- информация о фактическом адресе;
- информация о юридическом адресе.
А так же может быть всевозможная дополнительная информация.
В простом случае класс, реализующий моделируемую сущность, может выглядеть следующим образом.
namespace Domain;
final class Client
{
public function getId(): int;
public function setId($id): void;
public function setCorporateForm($corporateForm): void;
public function setName($name): void;
public function setGeneralManager(Manager $manager): void;
public function setCountry($country): void;
public function setCity($city): void;
public function setStreet($street): void;
public function setSubway($subway): void;
}
Целый ряд свойств мы опустили, для упрощения примера, реальный класс может быть значительно больше.
Полученная модель анемичная, перегружена ничего не значащими сеттерами вместо методов описывающих поведение соответствующее бизнес-требованиям. В любой момент, при таком подходе, можно создать объект в неконсистентном состоянии или нарушить бизнес-логику, установив один из параметров например так.
$client = new Client();
// В данный момент клиент у нас уже находится в не консистентном состоянии
// Если мы хотим запросить его идентификатор, то получим ошибку, т.к. он ещё не установлен
$client->getId();
// Или мы можем сохранить (попытаться) не валидного клиента, у которого не установлены обязательные свойства
$repository->save($client);
Создание и обеспечение консистентности сложных объектов-сущностей.
Для начала попробуем решить проблему консистентности. Для этого уберем из класса сеттеры, а все обязательные параметры будем запрашивать в конструкторе [4]. Таким образом, объект будет всегда валиден, может использоваться сразу после создания и обеспечивается полноценная инкапсуляция предотвращающая возможность приведения клиента в неконсистентное состояние. Теперь наш класс выглядит следующим образом.
namespace Domain;
final class Client
{
public function __construct(
$id,
$corporateForm,
$name,
$generalManager,
$country,
$city,
$street,
$subway = null
);
public function getId(): int;
}
Проблему мы решили, но получилось не слишком изящно. 8 параметров у конструктора, и это не предел. Конечно, далеко не каждая сущность требует так много обязательных параметров, это, пожалуй, не совсем рядовая ситуация, но и не необычная.
Что можно с этим сделать? Простое и очевидное решение — сгруппировать логически связанные параметры в объектах-значениях (Value Object) [3].
namespace Domain;
final class Address
{
public function __construct($country, $city, $street, $subway = null);
}
namespace Domain;
final class Client
{
public function __construct(
int $id,
string $name,
Enum $corporateForm,
Manager $generalManager,
Address $address
);
public function getId(): int;
}
Выглядит гораздо лучше, но параметров всё ещё довольно много, особенно это не удобно, если часть из них скалярные. Решение — шаблон Строитель (Builder) [5].
namespace Application;
interface ClientBuilder
{
public function buildClient(): Client;
public function setId($id): ClientBuilder;
public function setCorporateForm($corporateForm): ClientBuilder;
public function setName($name): ClientBuilder;
public function setGeneralManager(Manager $generalManager): ClientBuilder;
public function setAddress(Address $address): ClientBuilder;
}
$client = $builder->setId($id)
->setName($name)
->setGeneralManagerId($generalManager)
->setCorporateForm($corporateForm)
->setAddress($address)
->buildClient();
Таким образом, моделируемая сущность всегда находится в консистентном состоянии и при этом может быть гибко и понятно построена, не зависимо от сложности создаваемого объекта и количества параметров.
Создание объектов-сущностей с генерацией идентификатора по автоинкрементному полю базы данных.
У проектируемого класса обязательно должен быть уникальный идентификатор, т.к. основной отличительной чертой сущностей является индивидуальность. Объект может значительно изменяться с течением времени, так что ни одно из его свойств не будет равным тому, что было вначале. В то же время все или большинство свойств объекта могут совпадать со свойствами другого объекта, но это будут разные объекты. Именно уникальный идентификатор дает возможность различать каждый объект не зависимо от его текущего состояния [1].
Существуют различные способы формирования идентификаторов, такие как пользовательский ввод, генерация в приложении по какому-либо алгоритму, может генерироваться базой данных.
Cгенерировать, например, UUID или запросить у базы данных очередное значение последовательности не составляет никаких трудностей. Но иногда возникает необходимость работать с уже существующей структурой БД, в которой таблица хранящая соответствующие данные имеет для идентификатора автоинкрементное поле.
В таком случае крайне затруднительно и ненадежно получать очередной уникальный идентификатор без сохранения сущности, а значит невозможно создать объект в полностью консистентном состоянии.
И снова нам поможет в этом шаблон Строитель, который мы можем реализовать следующим образом.
namespace Infrastructure;
final class MySqlClientBuilder implements ClientBuilder
{
private $connection;
public function __construct(Connection $connection);
public function buildClient()
{
$this->connection
->insert('clients_table', [
$this->name,
$this->corporateForm,
$this->generalManager->getId(),
$this->address
]);
$id = $this->connection->lastInsertId();
return new Client(
$id,
$this->name,
$this->corporateForm,
$this->generalManager,
$this->address
);
}
}
Таким образом мы сначала добавляем соответствующую запись в базу данных, после чего получаем её идентификатор и создаем объект. Клиенту об этом знать не обязательно и при изменении способа хранения или генерации идентификаторов вам понадобится только заменить реализацию Строителя в вашем контейнере зависимостей.
$builder = $container->get(ClientBuilder::class);
$client = $builder->setId($id)
->setName($name)
->setGeneralManagerId($generalManager)
->setCorporateForm($corporateForm)
->setAddress($address)
->buildClient();
$repository->save($client);
$client->getId();
Благодарю за внимание!
P.S.:
Прошу не судить слишком строго, «чукча не писатель, чукча читатель». Для более опытных разработчиков описываемые вещи могут показаться банальными, я же к описанному пришел не сразу, но когда применил описанный подход, он зарекомендовал себя отлично.
Опыт разработки у меня относительно не большой — четыре года, DDD применял пока только на одном проекте.
Буду благодарен за отзывы и конструктивные замечания.
Ссылки:
- Эванс Э., «Предметно-ориентированное проектирование (DDD). Структуризация сложных программных систем.»
- Вернон В., «Реализация методов предметно-ориентированного проектирования.»
- М. Фаулер, Value Object
- М. Фаулер, Constructor Initialization
- Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидс, «Приёмы объектно-ориентированного проектирования. Паттерны проектирования.»
Автор: Sufir