DDD, репозиторий и инкапсуляция

Я заранее извиняюсь, если люди считают, что это было избито до смерти. Я только что провел последние несколько часов в поиске и чтении многих замечательных сообщений здесь, в SO, но я все еще смущен.

Источником моей путаницы является DTO и DDD и репозитории. Я хочу, чтобы у моих объектов домена POCO были умны, и я хочу получить их из репозиториев. Но мне кажется, что я должен нарушать некоторые правила инкапсуляции, чтобы сделать эту работу, и похоже, что она может превратить DTO в их головы.

Вот простой пример: В нашем приложении каталога часть может быть пакетом, который включает в себя ряд других частей. Таким образом, для Part POCO имеет смысл использовать метод GetChildren(), который возвращает IEnumerable < Часть > . Это может даже сделать другие вещи со списком на выходе.

Но как этот список разрешен? Похоже, репозиторий - это ответ:

interface IPartRepository : IRepository<Part>
{
    // Part LoadByID(int id); comes from IRepository<Part>
    IEnumerable<Part> GetChildren(Part part);
}

и

class Part
{
    ...
    public IEnumerable<Part> GetChildren()
    {
        // Might manipulate this list on the way out!
        return partRepository.GetChildren(this);
    }
}

Итак, теперь потребители моего каталога, помимо (правильно) загрузки частей из репозитория, могут также обходить некоторую часть инкапсулированной логики, напрямую обращаясь к GetChildren (part). Разве это не так плохо?

Я читал, что репозитории должны обслуживать POCO, но DTO хороши для передачи данных "между слоями". Вычисляются многие свойства деталей - цены, например, рассчитываются на основе сложных правил ценообразования. Цена не будет даже в DTO, поступающей из репозитория - так что передача данных о ценах обратно в веб-службу требует, чтобы DTO потреблял Part, а не наоборот.

Это уже слишком долго. Где моя голова отвинчена?

Ответ 1

Один из подходов, который решает эту проблему, состоит в том, чтобы переместить логику в сами дочерние части, т.е. изменить семантику класса, чтобы объекты Part отвечали за преобразование самих себя, когда они связаны с родителем.

Например, если цена a Part зависит от ее родительского Part, цена может быть определена в следующие моменты (как минимум):

  • При построении, если имеется родительский Part (и все другие необходимые данные).

  • В методе AttachToParent(Part parentPart) или в ответ на событие, т.е. OnAttachedToParent(Part parentPart).

  • Когда это необходимо клиентским кодом (т.е. при первом доступе к свойству Price).


Изменить: мой оригинальный ответ (см. ниже) действительно не был в духе DDD. Это включало создание простых объектов объектов домена, которые многие считают анти-шаблоном (см. Модель анемичного домена).

Дополнительный слой между Part и IPartRepository (я буду называть его IPartService) решает эту проблему: переместите GetChildren(part) в IPartService и удалите его из Part, затем сделайте вызов кода клиента IPartService, чтобы получить объекты Part и их дочерние элементы, а не напрямую ударять репозиторий. Класс Part по-прежнему имеет свойство ChildParts (или Children) - он просто не знает, как его заполнить сам.

Очевидно, что это налагает дополнительные затраты - вы можете в конечном итоге написать или создать много сквозного кода для вызовов репозитория, если в большинстве случаев вам не нужна дополнительная бизнес-логика.

Ответ 2

Недопустимая часть уравнения здесь - это поведение объекта Parts и то, как вы хотите работать с агрегатом. Вам нужно работать с отдельными детьми каждого из Part с n-й рекурсией или работать с "root" Part (т.е. Те, у кого нет родителей), и это дети в целом?

Наличие корневого агрегата Part, который содержит список довольно типично типизированных Parts, поскольку дети, похоже, не очень хорошо отображают вашу модель домена, но вы можете это сделать и рекурсивно ленить загружать каждую дочернюю коллекцию. Тем не менее, я все равно буду очень осторожен, когда возможна бесконечная рекурсия.

Что касается вашей второй проблемы, то DTO не предназначены для передачи данных между слоями столько, сколько они предназначены для переноса данных на ваш прикладной уровень и из него.

Они очень полезны, если вы используете сервис-ориентированную архитектуру (вы упоминаете веб-службы, но это может быть любая SOA). Ваши службы будут запрашивать ваши репозитории, выполнять какую-либо дополнительную работу, а затем сопоставлять объекты вашего домена в плоские DTO для отправки обратно запрашивающим клиентам. DTO должны быть простыми, не содержать никакой логической и прикладной функции, специфичной с целью сериализации.

Используйте объекты домена внутри приложения и внешние DTO.