в 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)?
codefish
источник
Что GetCustomerByName('John Smith')вернется, если в вашей базе данных будет двадцать Джонов Смитов? Похоже, вы предполагаете, что нет двух людей с одинаковыми именами.
bdsl

Ответы:

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>который называется Exposed, protected T GetByPredicate(Func<T, bool> predicate)который используется только в конкретных репозиториях (например, public class CustomerRepository : RepositoryBase<Customer>), тогда это будет хорошо.

MetaFight
источник
Итак, вы говорите, это нормально иметь классы DTO на уровне домена?
Codefish
Это не то, что я пытался сказать. Я не гуру DDD, поэтому я не могу сказать, приемлемо ли это. Из любопытства, почему бы тебе не вернуть целую сущность? Это слишком большой?
MetaFight
Ох, не совсем. Я просто хочу знать, что правильно и приемлемо делать. Чтобы вернуть полную сущность или только ее подмножество. Я думаю, это зависит от вашего ответа.
codefish
6

Существует значительное сообщество людей, которые используют CQRS для реализации своих доменов. Мне кажется, что если интерфейс вашего хранилища аналогичен лучшим методам, которые они используют, то вы не будете слишком далеко сбиваться с пути.

Основываясь на том, что я видел ...

1) Обработчики команд обычно используют репозиторий для загрузки агрегата через репозиторий. Команды предназначены для одного конкретного экземпляра агрегата; репозиторий загружает корень по ID. Нет, как я вижу, случая, когда команды запускаются для совокупности агрегатов (вместо этого сначала нужно выполнить запрос, чтобы получить коллекцию агрегатов, а затем перечислить коллекцию и выдать команду каждому.

Следовательно, в тех случаях, когда вы собираетесь изменять агрегат, я ожидаю, что хранилище вернет сущность (она же корень агрегата).

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 зависит от того, что вы ожидаете от потребителя. Я предполагаю, что будет одна конкретная реализация, поддерживающая интерфейс для каждого контекста.

VoiceOfUnreason
источник
Это все хорошо, но спрашивающий не упоминает об использовании CQRS. Вы правы, хотя, если бы он это сделал, его проблемы не было бы.
MetaFight
Я ничего не знаю о CQRS, но я слышал об этом. Насколько я понимаю, когда я думаю о том, что возвращать, если AggregateDTO или ProjectionDTO, я верну ProjectionDTO. Затем getCustomersThatSatisfy(Specification spec)я просто перечислю свойства, которые мне нужны для параметров поиска. Я правильно понял?
Codefish
Не понятно. Я не верю, что AggregateDTO должен существовать. Задача агрегата - убедиться, что все изменения удовлетворяют бизнес-инварианту. Это инкапсуляция правил домена, которые должны быть выполнены, то есть это поведение. Прогнозы, с другой стороны, представляют собой некий снимок состояния, приемлемого для бизнеса. См редактировать для попытки уточнить спецификацию.
VoiceOfUnreason
Я согласен с VoiceOfUnreason, что AggregateDTO не должен существовать - я думаю о 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, если вы еще этого не знаете.

Мухаммед Раджа
источник