Чи є елегантний спосіб перевірити унікальні протипоказання на атрибути об’єкта домену без переміщення бізнес-логіки в рівень обслуговування?


10

Я адаптував дизайн, орієнтований на домен, вже близько 8 років, і навіть після всіх цих років є ще одне, що мене клопоче. Це перевірка наявності унікального запису у сховищі даних щодо об’єкта домену.

У вересні 2013 року Мартін Фаулер згадав про принцип TellDon'tAsk , який, якщо можливо, слід застосувати до всіх об'єктів домену, які потім повинні повернути повідомлення про те, як пішла операція (в об'єктно-орієнтованому дизайні це здебільшого робиться за винятками, коли операція була невдалою).

Мої проекти зазвичай поділяються на багато частин, де дві з них - «Домен» (містить правила бізнесу та нічого іншого; домен - повністю наполегливий) і Сервіс. Послуги, що знають про рівень сховища, який використовується для даних CRUD.

Оскільки унікальність атрибута, що належить об'єкту, є правилом домену / бізнесу, воно повинно бути довгим модулем домену, тому правило саме там, де воно повинно бути.

Для того, щоб мати можливість перевірити унікальність запису, вам потрібно запитати поточний набір даних, як правило, базу даних, щоб дізнатися, чи існує ще одна запис із припущенням Name.

Зважаючи на те, що рівень домену є неосвіченим і не має уявлення про те, як отримати дані, але лише як робити операції над ними, він не може дійсно торкатися самих сховищ.

Дизайн, який я тоді адаптував, виглядає приблизно так:

class ProductRepository
{
    // throws Repository.RecordNotFoundException
    public Product GetBySKU(string sku);
}

class ProductCrudService
{
    private ProductRepository pr;

    public ProductCrudService(ProductRepository repository)
    {
        pr = repository;
    }

    public void SaveProduct(Domain.Product product)
    {
        try {
            pr.GetBySKU(product.SKU);

            throw Service.ProductWithSKUAlreadyExistsException("msg");
        } catch (Repository.RecordNotFoundException e) {
            // suppress/log exception
        }

        pr.MarkFresh(product);
        pr.ProcessChanges();
    }
}

Це призводить до того, що у вас є служби, що визначають правила домену, а не сам доменний рівень, і ви маєте правила, розповсюджені по декількох розділах коду.

Я згадав про принцип TellDon'tAsk, оскільки, як ви добре бачите, сервіс пропонує дію (вона зберігає Productабо викидає виняток), але всередині методу ви керуєте об'єктами за допомогою процедурного підходу.

Очевидним рішенням є створення Domain.ProductCollectionкласу Add(Domain.Product)методом викидання ProductWithSKUAlreadyExistsException, але це не вистачає продуктивності, тому що вам потрібно буде отримати всі Продукти зі сховища даних для того, щоб дізнатися в коді, чи продукт вже має ту саму SKU як Продукт, який ви намагаєтеся додати.

Як ви, хлопці, вирішуєте це конкретне питання? Це насправді не є проблемою, я вже мав службовий рівень представляти певні правила домену протягом багатьох років. Сервісний рівень зазвичай також обслуговує більш складні операції над доменом, мені просто цікаво, чи натрапили ви на краще, більш централізоване рішення протягом своєї кар’єри.


Навіщо тягнути цілий список імен, коли можна просто попросити одне і базувати логіку на тому, знайдено чи ні? Ви говорите шару наполегливості, що робити, а не як це робити, поки існує узгодження з інтерфейсом.
JeffO

1
Використовуючи термін Сервіс, ми повинні прагнути до більшої ясності, наприклад, до різниці між доменними службами на рівні домену та послугами додатків на рівні додатків. Дивіться gorodinski.com/blog/2012/04/14/…
Ерік Ейдт

Відмінна стаття, @ErikEidt, дякую. Я думаю, що моя конструкція не помиляється тоді, якщо я можу довіряти містерові Городинському, він майже все говорить те саме: Кращим рішенням є надання прикладного сервісу для отримання інформації, необхідної суб’єктом господарювання, ефективного налаштування середовища виконання та надання він є сутністю і має той самий об'єкт проти введення репозиторію в модель домену безпосередньо, як і я, в основному через порушення SRP.
Енді

Цитата з посилання "скажи, не питай": "Але особисто я не використовую" Tell-dont-ask ".
radarbob

Відповіді:


7

Зважаючи на те, що рівень домену є неосвіченим і не має уявлення про те, як отримати дані, але лише як робити операції над ними, він не може дійсно торкатися самих сховищ.

Я б не погодився з цією частиною. Особливо останнє речення.

Хоча це правда, що домен повинен бути неусвідомленим, але він знає, що існує "Колекція об'єктів домену". І що існують правила домену, які стосуються цієї колекції в цілому. Унікальність, будучи однією з них. І оскільки реалізація фактичної логіки сильно залежить від конкретного режиму стійкості, у домені повинна бути якась абстракція, яка визначає потребу в цій логіці.

Тож це так само просто, як створити інтерфейс, який може запитати, чи ім'я вже існує, яке потім реалізовується у вашому сховищі даних і викликається тим, хто повинен знати, чи ім'я унікальне.

І я хотів би підкреслити, що сховища - це послуги DOMAIN. Вони є абстракціями навколо наполегливості. Саме реалізація сховища має бути окремою від домену. Немає нічого поганого в тому, що суб'єкт домену викликає службу домену. Немає нічого поганого в тому, що одна організація може використовувати сховище для отримання іншого об'єкта або отримання певної інформації, яка не може бути легко збережена в пам'яті. Це причина, чому сховище є ключовим поняттям у книзі Еванса .


Дякую за вклад Чи може репозиторій насправді представляти те, що Domain.ProductCollectionя мав на увазі, вважаючи, що вони відповідають за отримання об'єктів із шару домену?
Енді

@DavidPacker Я не дуже розумію, що ти маєш на увазі. Тому що не повинно бути необхідним зберігати всі предмети в пам’яті. Реалізація методу "DoesNameExist" має бути (швидше за все) SQL-запитом на стороні сховища даних.
Ейфорія

Що означає, що замість того, щоб зберігати дані в колекції в пам'яті, коли я хочу знати весь Продукт, мені не потрібно їх отримувати, Domain.ProductCollectionа замість цього запитати у сховище, як і запитання, чи Domain.ProductCollectionмістить продукт з передав SKU, на цей раз знову запитуючи сховище (це насправді приклад), який замість ітерації над попередньо завантаженими продуктами запитує базову базу даних. Я не маю на увазі зберігати весь Продукт у пам’яті, якщо мені не доведеться, це було б повною дурницею.
Енді

Що призводить до іншого питання, чи повинен тоді сховище знати, чи повинен атрибут бути унікальним? Я завжди реалізовував сховища як досить німі компоненти, зберігаючи те, що ви передаєте їм, щоб зберегти, і намагаюся отримати дані на основі пройдених умов і поставив рішення в сервіси.
Енді

@DavidPacker Ну, "доменні знання" є в інтерфейсі. В інтерфейсі сказано, що "домену потрібна ця функціональність", і сховище даних надає цю функціональність. Якщо у вас є IProductRepositoryінтерфейс DoesNameExist, очевидно, що повинен робити метод. Але гарантування того, що Продукт не може мати таку ж назву, як існуючий продукт, має бути десь у домені.
Ейфорія

4

Потрібно прочитати Грега Янг про валідацію набору .

Коротка відповідь: перш ніж зайти занадто далеко в гніздо щурів, вам потрібно переконатися, що ви зрозуміли значення вимоги з точки зору бізнесу. Наскільки реально виявити дублювання та пом’якшити його, а не запобігти?

Проблема з "унікальністю" вимог полягає в тому, що, дуже часто, є глибша причина, чому люди хочуть їх - Ів Рейнхаут

Більш довга відповідь: я бачив меню можливостей, але всі вони мають компроміси.

Ви можете перевірити наявність дублікатів, перш ніж надсилати команду домену. Це можна зробити у клієнта або в сервісі (ваш приклад показує техніку). Якщо вас не влаштовує логіка, що витікає з доменного шару, ви можете досягти такого ж результату за допомогою a DomainService.

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

Звичайно, в такому разі впровадженню сервісу DeduplicationService потрібно буде знати щось про те, як шукати існуючий скіс. Тож, хоча це відсуває частину роботи назад у домен, ви все ще стикаєтеся з тими ж основними проблемами (вам потрібна відповідь на перевірку встановлених даних, проблеми з умовами перегонів).

Ви можете зробити валідацію у своєму шарі стійкості. Реляційні бази даних дійсно хороші у встановленій валідації. Покладіть унікальні обмеження на стовпчик sku свого продукту, і ви готові йти. Додаток просто зберігає продукт у сховищі, і ви отримуєте резервну копію порушення обмеження, якщо виникає проблема. Таким чином, код програми виглядає добре, і ваш стан перегонів усувається, але у вас є правила "домену", що витікають.

Ви можете створити окремий агрегат у своєму домені, який представляє набір відомих скісів. Тут я можу придумати дві варіанти.

Один - щось на кшталт ProductCatalog; продукти існують десь ще, але взаємозв’язок між продуктами та скусом підтримується каталогом, який гарантує унікальність ску. Не те, що це означає, що продукція не має скісу; skus призначається ProductCatalog (якщо вам потрібен унікальний skus, ви досягаєте цього, маючи лише один агрегат ProductCatalog). Перегляньте всюдисущу мову з експертами свого домену - якщо така річ існує, це може бути правильним підходом.

Альтернатива - щось більше, як служба бронювання ску. Основний механізм той самий: сукупність знає про всі скіси, тому може запобігти введенню дублікатів. Але механізм дещо інший: ви купуєте оренду на ску, перш ніж присвоювати його товару; при створенні товару ви передаєте його в оренду ску. У грі ще є умови перегонів (різні агрегати, отже, різні транзакції), але це має інший смак до цього. Справжнім недоліком тут є те, що ви проектуєте в доменну модель лізингову послугу, не маючи справжнього обґрунтування мовою домену.

Ви можете об'єднати всі товари продукції в єдиний агрегат - тобто, каталог товарів, описаний вище. Ви абсолютно отримуєте унікальність скісу, роблячи це, але вартість є додатковою суперечкою, модифікація будь-якого товару насправді означає модифікацію всього каталогу.

Мені не подобається необхідність витягувати всі артикули продукту з бази даних, щоб зробити операцію в пам'яті.

Можливо, не потрібно. Якщо ви протестуєте скубу за допомогою фільтра Bloom , ви зможете виявити безліч унікальних скісів, не завантажуючи набір взагалі.

Якщо ваш випадок використання дозволяє довільно щодо того, який сказ ви відхиляєте, ви можете виправити всі помилкові позитиви (не велика справа, якщо ви дозволите клієнтам перевірити запропонований ними скіс перед подачею команди). Це дозволить уникнути завантаження набору в пам'ять.

(Якщо ви хотіли б більше сприймати, ви можете вважати ледачим завантаження skus у разі відповідності у фільтрі розквітання; ви все одно ризикуєте завантажувати весь скіс у пам’ять, але це не має бути звичайним випадком, якщо ви дозволите код клієнта для перевірки команди на помилки перед відправкою).


Мені не подобається необхідність витягувати всі артикули продукту з бази даних, щоб зробити операцію в пам'яті. Це явне рішення, яке я запропонував у початковому питанні та поставив під сумнів його ефективність. Крім того, IMO, покладаючись на обмеження у вашій БД, нести відповідальність за унікальність, є поганим. Якщо ви перейшли на новий двигун баз даних і якось унікальне обмеження загубилося під час трансформації, ви зламали код, оскільки інформації про базу даних, яка там була раніше, вже немає. Все одно дякую за посилання, цікаве прочитання.
Енді

Можливо, фільтр Bloom (див. Редагування).
VoiceOfUnreason
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.