Введення в проектування сутностей, проблеми створення об'єктів

При моделюванні такого поняття предметно-орієнтованого проектування як сутність можуть виникнути деякі складності, обумовлені бізнес-вимог або технічною частиною. Зокрема, іноді виникає складність із створенням об'єкта-сутності.

У даній статті описуються дві такі проблеми, і розглядається спосіб їх вирішення. Так само стаття підійде як введення в проектування сутностей. Для розуміння матеріалу знадобиться базове уявлення про предметно-орієнтованому проектуванні.

Отже, ми вивчили предметну область, сформували єдиний мову, виділили обмежені контексти та визначилися з вимогами [2]. Все це виходить за рамки даної статті, тут ми спробуємо вирішити конкретні вузькі проблеми:

  1. Створення та забезпечення консистентности складних об'єктів-сутностей.
  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 параметрів у конструктора, і це не межа. Звичайно, далеко не кожна сутність вимагає так багато обов'язкових параметрів, це, мабуть, не зовсім рядова ситуація, але і не незвичайна.

Що можна з цим зробити? Просте і очевидне рішення — згрупувати логічно пов'язані параметри в об'єктах-значеннях (Object Value) [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].

Існують різні способи формування ідентифікаторів, такі як користувальницький введення, генерація у програмі якого-небудь алгоритму, може генеруватися базою даних.

Згенерувати, наприклад, 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 застосовував поки тільки на одному проекті.

Буду вдячний за відгуки та конструктивні зауваження.


Посилання:

  1. Еванс Е., «Предметно-орієнтоване проектування (DDD). Структуризація складних програмних систем.»
  2. Вернон Ст., «Реалізація методів предметно-орієнтованого проектування.»
  3. М. Фаулер, Value Object
  4. М. Фаулер, Constructor Initialization
  5. Е. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидс, «Прийоми об'єктно-орієнтованого проектування. Патерни проектування.»
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.