У DDD, чи повинні сховища відкривати об'єкти чи домен?


11

Як я розумію, в DDD доречно використовувати шаблон сховища з сукупним коренем. Моє запитання полягає в тому, чи слід повертати дані як об'єкт або доменні об'єкти / DTO?

Можливо, якийсь код пояснить моє питання далі:

Суб'єкт

public class Customer
{
  public Guid Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

Чи варто робити щось подібне?

public Customer GetCustomerByName(string name) { /*some code*/ }

Або щось подібне?

public class CustomerDTO
{
  public Guid Id { get; set; }
  public FullName { get; set; }
}

public CustomerDTO GetCustomerByName(string name) { /*some code*/ }

Додаткове запитання:

  1. Чи повинен я повернути у сховище IQueryable або IEnumerable?
  2. У службі або сховище, я повинен зробити що - щось на зразок .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? чи просто зробити метод, який є чимось на кшталт GetCustomerBy(Func<string, bool> predicate)?

Що GetCustomerByName('John Smith')повернеться, якщо у вашій базі буде двадцять Джона Сміта? Схоже, ви припускаєте, що жодна людина не має одного імені.
бдсл

Відповіді:


8

чи повинен я повертати дані у вигляді об'єктів чи об'єктів домену / DTO

Добре, що повністю залежить від ваших випадків використання. Єдиною причиною, яку я можу подумати про повернення DTO замість повного об'єкта, є те, якщо ваша сутність величезна, і вам потрібно працювати лише над її підмножиною.

Якщо це так, то, можливо, вам слід переглянути свою модель домену та розділити свою велику сутність на пов'язані менші сутності.

  1. Чи повинен я повернути у сховище IQueryable або IEnumerable?

Добре правило - завжди повертати найпростіший (найвищий в ієрархії спадщини) тип. Отже, повертайтеся, IEnumerableякщо ви не хочете дозволити споживачеві сховища працювати з IQueryable.

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

  1. Чи повинен я зробити в сервісі чи сховищі щось на кшталт .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? або просто зробити метод, який є чимось на зразок GetCustomerBy (предикат Func)?

З тієї ж причини, яку я згадував у пункті 1, точно не використовуйте GetCustomerBy(Func<string, bool> predicate). Спочатку це може здатися спокусливим, але саме тому люди навчилися ненавидіти загальні сховища. Це герметично.

Такі речі GetByPredicate(Func<T, bool> predicate)корисні лише тоді, коли вони ховаються за конкретними заняттями. Отже, якби у вас був абстрактний базовий клас, RepositoryBase<T>який називався, який відкрився, protected T GetByPredicate(Func<T, bool> predicate)який використовувався лише конкретними сховищами (наприклад, public class CustomerRepository : RepositoryBase<Customer>), то це було б добре.


Отже, ваше висловлювання, нормально мати класи DTO у доменному шарі?
кодова риба

Це не те, що я намагався сказати. Я не гуру DDD, тому не можу сказати, чи прийнятно це. З цікавості, чому б вам не повернути повну сутність? Він занадто великий?
MetaFight

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

6

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

На основі побаченого ...

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

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

2) Обробники запитів взагалі не торкаються агрегатів; натомість вони працюють із проекціями агрегатів - об'єктів цінності, які описують стан агрегату / агрегатів у певний момент часу. Тож подумайте ProjectionDTO, а не AggregateDTO, і ви маєте правильну ідею.

У контекстах, де ви збираєтеся виконувати запити проти сукупності, готувати її до відображення тощо, я очікував би повернення DTO або колекції DTO, а не сутність.

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

getCustomersThatSatisfy(Specification spec)

є розумним вибором; Потім обробники запитів побудують відповідну специфікацію з заданих параметрів і передадуть цю специфікацію в сховище. Мінусом є те, що підпис дійсно говорить про те, що сховище - це колекція в пам'яті; мені незрозуміло, що предикат купує вам багато, якщо репозиторій є лише абстракцією запуску оператора SQL проти реляційної бази даних.

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

Попередження: виявлено java типу введення тексту

interface CustomerRepository {
    interface ConstraintBuilder {
        void setLastName();
        void setFirstName();
    }

    interface ConstraintDescriptor {
        void copyTo(ConstraintBuilder builder);
    }

    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor);
}

SQLBackedCustomerRepository implements CustomerRepository {
    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
        WhereClauseBuilder builder = new WhereClauseBuilder();
        descriptor.copyTo(builder);
        Query q = createQuery(builder.build());
        //...
     }
}

CollectionBackedCustomerRepository implements CustomerRepository {
    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
        PredicateBuilder builder = new PredicateBuilder();
        descriptor.copyTo(builder);
        Predicate p = builder.build();
        // ...
}

class MatchLastName implements CustomerRepository.ConstraintDescriptor {
    private final lastName;
    // ...

    void copyTo(CustomerRepository.ConstraintBuilder builder) {
        builder.setLastName(this.lastName);
    }
}

На закінчення: вибір між наданням агрегату та наданням DTO залежить від того, що ви очікуєте від споживача. Моя здогадка - це одна конкретна реалізація, що підтримує інтерфейс для кожного контексту.


Це все добре і добре, але запитувач не згадує про використання CQRS. Ти маєш рацію, хоча б його проблема не була.
MetaFight

Я не знаю про CQRS, тому я чув про це. Як я це розумію, коли я думаю про те, що повернути, якщо AggregateDTO або ProjectionDTO, я поверну ProjectionDTO. Потім у списку getCustomersThatSatisfy(Specification spec)я просто перелічу властивості, необхідні для пошуку варіантів. Я правильно розумію?
кодова риба

Незрозуміло. Я не вірю, що AggregateDTO має існувати. Суть сукупності полягає в тому, щоб забезпечити, що всі зміни задовольняють бізнес-інваріант. Це інкапсуляція правил домену, яких потрібно виконувати - тобто це поведінка. З іншого боку, прогнози - це уявлення про деякий момент стану, прийнятного для бізнесу. Дивіться правки для спроби уточнити специфікацію.
VoiceOfUnreason

Я погоджуюся з VoiceOfUnreason, що агрегатний DTO не повинен існувати - я вважаю ProjectionDTO як перегляд моделей лише для даних. Він може адаптувати свою форму до того, що вимагає абонент, при необхідності перетягуючи дані з різних джерел. Кореневий агрегат повинен представляти собою повне представлення всіх пов'язаних даних, щоб усі референтні правила можна було перевірити перед збереженням. Він змінює форму лише за більш різких обставин, таких як зміни таблиці БД або змінені відносини даних.
Бред Ірбі

1

Виходячи з моїх знань про DDD, ви повинні мати це,

public CustomerDTO GetCustomerByName(string name) { /*some code*/ }

Чи повинен я повернути у сховище IQueryable або IEnumerable?

Залежить, ви знайдете змішані погляди різних народів на це питання. Але я особисто вважаю, що якщо ваш сховище буде використовуватися такою службою, як CustomerService, то ви можете використовувати IQueryable, інакше дотримуйтесь IEnumerable.

Чи повинні репозиторії повертати IQueryable?

Чи повинен я зробити в сервісі чи сховищі щось на кшталт .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? або просто зробити метод, який є чимось на зразок GetCustomerBy (предикат Func)?

У вашому сховищі у вас повинна бути загальна функція, як ви сказали, GetCustomer(Func predicate)але до рівня обслуговування додайте три різні методи. Це тому, що є ймовірність, що ваша служба викликається від різних клієнтів, і вони потребують різних DTO.

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

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