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

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

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

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

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

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 использовал часть, а не наоборот.

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


person n8wrl    schedule 17.07.2009    source источник


Ответы (2)


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

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

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

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

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


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

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

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

person Jeff Sternal    schedule 17.07.2009
comment
Интересный. Но меня смущает «переместить GetChildren (часть) в IPartService и удалить его из части», а затем «Класс Part все еще имеет свойство Childparts». Что, если по какой-то причине части нужно сделать массаж своим дочерним элементам? - person n8wrl; 17.07.2009
comment
Если массирование должно происходить немедленно, когда вы извлекаете его из репозитория, я бы поместил эту логику в файл IPartService.GetChildren(). Если вам нужно иметь возможность изменять дочерние части в произвольное время, вы можете создать другой сервисный метод, такой как IPartService.UpdateChildPartPrices(Part part). (или оба — вы можете вызвать UpdateChildPartPrices из GetChildren.) - person Jeff Sternal; 17.07.2009
comment
Спасибо, что нашли время, Джефф! - person n8wrl; 17.07.2009
comment
Я надеюсь, что вы получите больше ответов, это неприятный, интересный вопрос. Я обновил свой ответ, чтобы учесть противоречивый характер исходного предложения (и признать его обязательства). - person Jeff Sternal; 17.07.2009

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

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

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

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

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

person Mike Gardiner    schedule 04.08.2009