Страницы

Поиск по вопросам

вторник, 10 декабря 2019 г.

DDD repository/facade implementation + bounded context relationship

#c_sharp #шаблоны_проектирования #orm #repository #ddd


Вопрос в следующем, как лучше реализовать механизм Include?

Предположим, у нас есть Repository и нам требуется загрузить сущность со связанными
элементами, в сервисе мы можем использовать следующий код:

repo.Select().Include(sc => sc.Students).Include(sc => sc.Teachers);


или даже переопределить операцию select и передавать коллекцию Include Expression
в качестве параметра:

 repo.Select(sc => sc.Students, sc => sc.Teachers);


Cо временем наша школа становится платной, и мы создаем другой Bounded Context, в
котором реализована логика по оплате / начислениям.

Еще через время нам понадобилось подружить две модели и мы хотим в нашем классе Student
добавить NotMapped поле, которое бы определяло,например, имеет ли студент доступ к
библиотеке (логика определения находится в другой модели).

И нам, в момент получения данных, нужно сделать инъекцию для класса Student:

Student student = repo.Select(p => p.Id == id);
bool access = ChargeService.GetBalanceInfo(id);
student.Init(access);
return student;


Теперь о проблеме, когда ты используешь Include в коде самого сервиса,тебе придется
переписать кучу кода, чтобы сделать такую инъекцию.

Решение проблемы напрашивается - сделать отдельный метод в репозитории:

IEnumerable GetSchoolWithStudentsAndTeachers();


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

Можете подсказать, как бы вы поступили/поступаете в данной ситуации?  
    


Ответы

Ответ 1



В Вашем описании Student это класс предметной области, однако, он загружается непосредственно из базы с помощью Entity Framework. Entity Framework не может устанавливать значения свойств только для чтения, так что Вы должны сделать все свойства доступными для изменения. Но в предметной области некоторые поля неизменяемые, например, идентификатор сущности, дата и время создания сущности, и другие. Некоторые поля должны изменяться согласованно, например, новое состояние сущности и дата/время её последнего изменения. Именно такое согласованное изменение и называется инкапсуляцией. Student, у которого все поля открыты, можно назвать анемичной моделью, которую, например, Фаулер считает анти-паттерном (Anemic Domain Model). Было бы правильно спрятать состояние полностью внутрь, а снаружи оставить только методы, которые согласованно меняют состояние сущности. С чем же тогда будет работать Entity Framework? С Data Transfer Object'ом, который отражает состояние сущности: [Table("Student")] public class StudentDto { public int Id { get; set; } public int SchoolId { get; set; } public virtual SchoolDto School { get; set; } . . . } public class Student { private readonly StudentDto dto; internal Student(StudentDto dto) { this.dto = dto; School = new School(dto.School); } public int Id => dto.Id; public School School { get; } } В данном случае мы видим, что при загрузке студента из базы надо включить навигационное поле School. Это делает реализация класса-репозитория: public class EFStudentRepository: IStudentRepository { private readonly DbContextScope dbContextScope; public EFStudentRepository(DbContextScope dbContextScope) { this.dbContextScope = dbContextScope; } public Student ReadById(int id) { var dto = dbContextScope.Context .Students. .Include(x => x.School) .SingleOrDefault(x => x.Id == id); if (dto == null) throw new EntityNotFound(typeof(Student), id); return Student.From(dto); } } А вот как указывать репозиторию, какие навигационные свойства включать, а какие нет? Ответ DDD: это вопрос из другого уровня приложения. В предметной области нет понятия включенных классов, это термин уровня доступа к данным, причём термин конкретной библиотеки Entity Framework. В DDD есть понятие составных сущностей: агрегатов. Например, заказ и товарные позиции в этом заказе. В базе данных это две разных таблицы, а на уровне предметной области: класс Order со свойством коллекцией Products. И заказ, и товарная позиция суть сущности, но заказ это корневая сущность агрегата, а товарная позиция просто в него входит. Значит, при загрузке из базы надо загружать корень агрегата и все данные, которые должны быть в агрегате в рамках данного Bounded Context. Но считается, что одни агрегаты не должны содержать другие агрегаты, на них можно ссылаться только по идентификатору. Поскольку School — это, вероятно, отдельный агрегат, у Student должно быть свойство SchoolId вместо School. Такое ограничение позволяет упростить слой доступа к данным. С другой стороны, из-за этого приходится чаще посылать к базе отдельные запросы для загрузки связанных агрегатов. Это цена, которая в DDD платится за то, чтобы сделать код понятнее и чище. Таким образом, ответ из DDD звучит так: опишите агрегаты предметной области, достаточные для реализации её сценариев. Загрузку независимых агрегатов опишите в репозиториях в явном виде. Например, сценарий требует получения всех учителей студента, а учитель и студент — независимые агрегаты. В этом случае в репозитории учителей надо реализовать метод загрузки всех учителей по идентификатору студента: public interface ITeacherRepository { IReadOnlyCollection ReadAllByStudentId(int studentId); } Сервисы загружают все необходимые данные и выполняют групповую операцию. На уровне сервисов также задаются транзакции, если это нужно.

Ответ 2



Прошу прощения, если не в тему. Я из мира PHP и сталкивался с похожей проблемой, если я правильно ее понял. Насколько я понял, проблема в том, как изменять запросы в результате естественного развития проекта так, чтобы ничего не потерять и репозитории не разрастались. Один из способов решения этой задачи - это шаблон проектирования Спецификация (пример в вики возможно не самый удачный). Вот еще пример на .NET и на PHP. А вот пример реализации библиотеки на PHP (под C# тоже должно быть что-то такое). Возможно поможет понять общую идею. Вкратце. Мы создаем простые классы спецификаций и компонуя их получаем требуемый результат. Очень грубо по вашему промеру: repo.match(new AndSpec( new SchoolSpec(), new JoinStudents(), new JoinTeachers() )) три разных спецификации группируются в одну. Можно вызывать такую группировку из контроллера или из Query в контексте CQRS как предложил @Fynivx. Можно 3 спецификации объединить в одну большую SchoolWithStudentsAndTeachersSpec. Надеюсь это то что вас интересует. PS: Говоря о Bounded Context и DDD, я все больше прихожу к мысли, что в каждом контексте должны быть свои сущности и они ни как не должны взаимодействовать с сущностями в других контекстах. Статья на хабре о микросервисах натолкнула меня на мысль что Bounded Context нужно рассматривать как потенциальный микросервис. И соответственно связи между контекстами должны быть минимизированы. А если нам нужно в одном контексте использовать сущности из другого контекста, то мы должны использовать адапторы или маппить БД на свои собственные сущности которые отвечают требованиям нашего контекста. Суперглобальные агрегаты это плохо. Вопросы практического использования DDD хорошо описаны в книге Вон Вернона.

Комментариев нет:

Отправить комментарий