Домен-ориентированный дизайн: иерархии, репозитории и доступ - PullRequest
0 голосов
/ 09 июля 2020

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

У меня есть следующие иерархии:

  • A Section может иметь 0 или более Lesson
  • A Lesson может иметь 0 или более Resource

Идентификаторы всех сущностей глобально уникальны.

Каждая сущность действует как их собственные агрегаты, но содержат ссылки на другие агрегаты. Например: Lesson ссылается на Section по его идентификатору.

Если мы представим класс для Урока, это будет примерно так:

class Lesson {
    id : string,
    sectionId : string,
    order: number,
    title: string,
    description: string,
    resources: Resource[]
}

И если мы представьте, что класс для раздела выглядит примерно так:

class Section {
    id: string,
    lessons: Lesson[]
}

Теперь я могу изменить имя или описание Lesson без доступа к Section (поскольку идентификаторы глобальные), потому что нет инвариантного правила для перерыв. Но если я изменяю порядок Lesson, имеет смысл сделать это через Section, потому что мне нужно отслеживать уроки в конкретном разделе, чтобы поддерживать инвариант.

Сказав это, вот мои сомнения:

  1. Все модификации проходят через класс Section. В этом сценарии, если я выполняю UpdateLessonTitleUseCase , я думаю, это может выглядеть так:

Option A)

let mySection = SectionRepository.getById(sectionId);

Section.updateLessonTitle(lessonId, 'new title');

SectionRepository.save(mySection);

Но если мне нужно предоставить метод для каждого свойства, это больно (потому что дерево может расти очень глубоко или у определенного класса есть много свойств). Другой вариант может быть таким:

Вариант B)

let mySection = SectionRepository.getById(sectionId);

let myLesson = mySection.getLessonById(lessonId);

myLesson.setTitle('new title');

mySection.updateLesson(myLesson);

SectionRepository.save(mySection);

Если я выполняю ChangeLessonsOrderUseCase , это будет что-то вроде это:

let mySection = SectionRepository.getById(sectionId);

mySection.updateLessonsOrder(orderData); // orderData is an array of {lessonId: string, order: number}

SectionRepository.save(mySection);

Если я выполняю UpdateResourceLinkUseCase , это будет примерно так:

let mySection = SectionRepository.getById(sectionId);

let myLesson = mySection.getLessonById(lessonId);

let myResource = myLesson.getResourceById(resourceId);

myResource.setLink('new link'); 

myLesson.updateResource(myResource); 

mySection.updateLesson(myLesson); 

SectionRepository.save(mySection);

В этом сценарии Lesson не может быть собственной совокупностью, верно? Потому что Section может возвращать только версию других агрегатов, доступную только для чтения, и, кроме того, SectionRepository не должен обращаться к LessonRepository, верно? Итак, у нас будет только совокупность, а это Section, а SectionRepository позаботится о сохранении всего. И, если бы мы go поднялись по дереву (или спустились по дереву), у нас было бы намного больше вещей для хранения.

Любая помощь приветствуется, если подумать об этом. Спасибо!

Ответы [ 2 ]

0 голосов
/ 11 июля 2020

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

Из этой строки кода, которой вы поделились:

let myResource = ResourceRepository.getResourceById(resourceId);

myResource.setLink('new link');

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

Итак, с моей точки зрения Ресурс - это то, на что можно ссылаться в разных уроках. Если это предположение применимо, вам следует подумать о том, чтобы сделать вашу ссылку самостоятельным агрегатом. Тогда вы сохраните только список идентификаторов ссылок в своем уроке. Я обычно использую явный объект-значение для представления строго типизированной ссылки на сущность root, например класса объекта-значения «ReferenceId».

Таким образом, ваш класс Lesson вместо этого будет выглядеть примерно так:

class Lesson {
    id : string,
    sectionId : string,
    order: number,
    title: string,
    description: string,
    resources: ResourceId[]
}

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

Этот подход дает еще одно преимущество: если вы ссылаетесь на один и тот же ресурс в разных уроках, вам не нужно обновлять его везде, поскольку на ресурс ссылается только идентификатор. Конечно, вам нужно изменить свой подход при предоставлении данных, поскольку вы храните только ссылку. Но по моему опыту, фактические данные во многих случаях требуются только при просмотре данных в некотором интерфейсе, а не для выполнения logi c домена. Таким образом, когда вы знаете ссылки, при рендеринге ваших моделей представления становится проще, поскольку вам действительно не нужно go через репозитории домена, когда дело касается только просмотра данных, но вы можете получить доступ ко всем данным из своей базы данных так, как вам это нужно. При просмотре урока идентификатор ресурса позволяет вам получать любые данные ресурса, необходимые для визуализации.

Имейте в виду, что такой подход может иметь или не иметь смысла в зависимости от требований вашего бизнеса. Но если отношение Урок -> Ссылка каким-то образом сопоставимо с отношением Заказ -> Пользователь , я думаю, что это может быть жизнеспособным вариантом для вас.

Другой вариант был бы думать о своих Уроках как о совокупности корней. Это также даст вам возможность компилировать курсы, возобновляя одни и те же уроки в разных курсах и, следовательно, разделы разных курсов. информация, такая как заголовок, в зависимости от ваших потребностей) в разделах. Обычно возникает вопрос, какая информация действительно нужна агрегату для правильного применения его бизнес-правил и инвариантов.

Обновление

Я хочу подробнее рассказать о дополнительных вопросы, которые вы задали в своем комментарии.

1.) Кто отвечает, например, за извлечение фактических ресурсов из уроков?

Если вы просто хотите получить данные для просмотра раздела или урока в пользовательском интерфейсе. Я хочу использовать модель чтения. В этом случае вы можете полностью обойти агрегированные репозитории, так как вы, чтобы не изменять какие-либо данные, выполнение доступа для чтения к вашей базе данных должно происходить таким образом, который наиболее подходит для просмотра данных. Это полностью допустимый подход, который также предотвращает нежелательные проблемы с производительностью, поскольку загрузка агрегатов через репозитории домена обычно предназначена для выполнения согласованных бизнес-транзакций с сохранением неизменных бизнес-логических c инвариантов. Также в большинстве случаев он обычно фокусируется на изменении одного агрегата.

Если вам нужно выполнить бизнес-операцию на агрегате Урок и вам нужны данные ресурса (кроме только идентификатора ресурса), я бы использовал репозиторий ресурсов для загрузки ресурса на основе его идентификатора через репозиторий ресурсов, а затем передать необходимые данные для соответствующей операции домена Урока.

Например, если вы обрабатываете запрос авторов на изменение, скажем, название ресурса в этом уроке (учитывая, что у вас разные названия одного и того же ресурса в Уроке) , Я бы использовал объект значения, например LessonResource. Этот объект значения будет содержать ссылочный идентификатор агрегата ресурса и заголовок, который используется только внутри этого урока.

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

let resource = resourceRepository.findById(changeResourceTitleCommand.resourceId);
let lesson = lessonRepository.findById(changeResourceTitleCommand.lessonId);

lesson.changeResourceTitle(
    changeResourceTitleCommand.resourceId, 
    changeResourceTitleCommand.title,
    resource
);
lessonRepository.save(lesson);

Примечание : ChangeResourceTitleCommand будет некоторым простым DTO, представляющим данные, переданные, например, через запрос REST, который содержит только данные, необходимые для этой операции.

В этом случае давайте рассмотрим вашу операцию домена Урока (changeResourceTitle ()) также требуется информация из ресурса. Например, информацию о том, следует ли использовать для этого Ресурса глобально согласованный заголовок, который должен использоваться на всех Уроках. Возможно, это не лучший пример из реальной жизни, но я надеюсь, что вы уловили идею: -)

2.) И, предположим, у меня есть вариант использования для изменения порядок занятий внутри раздела. В разделе хранится идентификатор уроки, а уроки необходимо изменить и сохранить. Должен ли домен раздела получать доступ к LessonRepository?

В этом случае я бы использовал объект значения SectionLesson, который содержит идентификатор урока, а также порядок (позицию).

В В этом случае вам даже не нужно получать агрегированные данные Урока для выполнения операции домена с агрегатом Раздела.

Вариант использования может выглядеть следующим образом:

let section = sectionRepository.findSectionById(changeSectionOrderCommand.sectionid);
section.changeLessonOrder(
    changeSectionOrderCommand.lessionId,
    changeSectionOrderCommand.position
);
sectionRepository.save(section);

Ваше изменениеLessonOrder ()

Примечание : в зависимости от вашей реализации и требований может быть даже более целесообразным отправить весь новый упорядоченный список уроков как определено пользователем (автором) в пользовательском интерфейсе серверной части. В любом случае вам также понадобится отсортированный список идентификаторов уроков для выполнения операции с доменом раздела без необходимости доступа к любому LessonRepository.

0 голосов
/ 09 июля 2020

В вашем случае Section выглядит как агрегат root. Так что, если это то, что вы хотите, вы должны пропустить его для всех действий, которые вы хотите применить к своей совокупности, и сохранить его.

Я думаю, что вы правы с вариантом A, возможно, вам следует глубже изучить свой домен и выяснить, почему вам необходимо обработать каждое свойство каждого объекта Entity / Value вашего агрегата.

Для вашего варианта использования UpdateResourceLinkUseCase Я попробую что-то вроде этого

let mySection = SectionRepository.getById(sectionId);

mySection.setRessourceLink(lessonId,resourceId,'new link'); 

SectionRepository.save(mySection);
...