Slide 1

Slide 1 text

Практический опыт реализации сложной предметной логики с помощью агрегатов Вячеслав Муравлев Руководитель группы разработки JPoint Москва, 6 апреля 2019

Slide 2

Slide 2 text

Немного о себе | Руковожу группой разработки в проекте автоматизации образовательного процесса в вузах | В энтерпрайз-разработке с 2004 года | В основном занимаюсь бэкенд-разработкой 2 18

Slide 3

Slide 3 text

Немного о проекте | Облачное решение, рассчитанное на много вузов | Различные категории пользователей | Большая и сложная предметная область | Используем domain-driven design 3 18

Slide 4

Slide 4 text

Архитектура ФПС: Ведение каталога курсов ФПС: Планирование учебных периодов … Domain-level REST API Application-level REST API Frontend BE-1 BE-n BE-2 Gateway Frontend BE-1 BE-n BE-2 Gateway 4 18

Slide 5

Slide 5 text

Типовой способ реализации предметной логики @Table (not null, foreign key) @Entity (get/set) @Repository (CRUD) @Service (сделай все бизнес-проверки ну и все остальное) @RestController (вызови сервисы и репозитории в транзакции и отдай JSON) Мало логики Много логики 5 18

Slide 6

Slide 6 text

Насыщаем модель предметной логикой: выделяем агрегаты | Переносим в доменные объекты операции с тесно связанными объектами | Бизнес-правила и инварианты – в доменные объекты | Анализируем связи между сущностями 6 18

Slide 7

Slide 7 text

Агрегаты в предметной области Состояние курса Технология реализации курса Образовательный результат курса Сотрудник Учебный курс 7 18 Инвариант В состоянии «Опубликован» должна быть указана технология реализации курса

Slide 8

Slide 8 text

Переносим логику в доменные объекты @Table Агрегат @Repository @Service @RestController Мало логики Много логики Агрегат Агрегат 8 18

Slide 9

Slide 9 text

Rich API: когда и как | Используем для связанных объектов со сложной структурой и своим жизненным циклом | Все манипуляции со связанными объектами – только через API корневого объекта | Соблюдение инвариантов в корневом объекте 9 18

Slide 10

Slide 10 text

Rich API: модель 10 18

Slide 11

Slide 11 text

Rich API: работа с вложенными сущностями 11 18 API для работы с вложенными сущностями Корневая сущность Часть агрегата: связанная сущность @Entity @Table(name = "course_unit_lesson") public class Lesson extends AuditableEntity { @Embedded @AssociationOverride(name = "requirementContainers", joinColumns = @JoinColumn(name = "lesson_id")) private LessonRequirementsCollection requirements = new LessonRequirementsCollection(); public LessonRequirementId createRequirement(LessonRequirement reqData) { LessonRequirementContainer reqContainer = new LessonRequirementContainer(args); requirements.add(reqContainer); reqData.initFromLesson(this); return reqContainer.getLessonRequirementId(); } public void replaceRequirement(String reqId, LessonRequirement reqData) throws LessonRequirementMissingException { requirements.replace(reqId, reqData); } public LessonCopyResult copy(String lessonId, CourseUnit targetCourseUnit, boolean copyRequirements) { // copy internal objects, check invariants… } }

Slide 12

Slide 12 text

Развитие модели 12 18

Slide 13

Slide 13 text

Value objects: когда и как | Используем для связанных объектов со сложной структурой и без жизненного цикла | Сериализуем value object в JSON и сохраняем в БД | Используем JPA AttributeConverter 13 18

Slide 14

Slide 14 text

Соблюдение инвариантов с помощью событий | Используем для разрастающихся агрегатов с вложенными сущностями | Переносим часть API во вложенные сущности | Используем событийные механизмы для соблюдения целостности 14 18

Slide 15

Slide 15 text

@Entity @Table(name = "planned_cohort") @EntityListeners({CourseUnitRealizationPartEntityListener.class}) public class PlannedCohort extends CourseUnitRealizationPart {} public class CourseUnitRealizationPartEntityListener { @PrePersist @PreUpdate @PreRemove public void incrementCourseUnitRevision(CourseUnitRealizationAware entity) { entity.courseUnitRealization().checkInvariants(); } } Соблюдение инвариантов с помощью Hibernate 15 18 Корневая сущность агрегата: план проведения курса Проверяем инварианты Hibernate listener Часть агрегата: поток обучения @Entity @Table(name = "course_unit_realization") @EntityListeners({CourseUnitRealizationPartEntityListener.class}) public class CourseUnitRealization { @OneToMany(fetch = FetchType.LAZY, mappedBy = "courseUnitRealization", cascade = CascadeType.ALL, orphanRemoval = true) private Set cohorts = new HashSet<>(0); @OneToMany(fetch = FetchType.LAZY, mappedBy = "courseUnitRealization") private Set planElements = new HashSet<>(0); }

Slide 16

Slide 16 text

Как мы делаем агрегаты | Делаем насыщенный API для корневых объектов | Храним value objects целиком в БД | Соблюдаем инварианты с помощью событийных механизмов 16 18

Slide 17

Slide 17 text

Рекомендую почитать | Эрик Эванс «Предметно-ориентированное проектирование (DDD): структуризация сложных программных систем» | Вон Вернон «Реализация методов предметно- ориентированного проектирования» 17 18

Slide 18

Slide 18 text

Спасибо за внимание! Вячеслав Муравлев vmuravlev@custis.ru

Slide 19

Slide 19 text

Value object: модель

Slide 20

Slide 20 text

Value object: делаем сам объект @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type") @JsonSubTypes({ @JsonSubTypes.Type(value = LessonClassroomRequirement.class, name = LessonRequirementType.TYPE_CLASSROOM), @JsonSubTypes.Type(value = LessonProvisionRequirement.class, name = LessonRequirementType.TYPE_LESSON_PROVISION), @JsonSubTypes.Type(value = LessonTeacherRoleRequirement.class, name = LessonRequirementType.TYPE_TEACHER_ROLE), @JsonSubTypes.Type(value = LessonSchedulingIntervalFromPrevLessonRequirement.class, name = LessonRequirementType.TYPE_SCHEDULING_INTERVAL_FROM_PREV_LESSON), @JsonSubTypes.Type(value = LessonTimeRequirement.class, name = LessonRequirementType.TYPE_TIME), @JsonSubTypes.Type(value = LessonPreparationRequirement.class, name = LessonRequirementType.TYPE_PREP), }) public abstract class LessonRequirement implements Serializable { public void initFromLesson(Lesson lesson) { // для установки данных из родительской УВ } public abstract LessonRequirement copy(); }

Slide 21

Slide 21 text

Value object: используем в сущности @Entity @Table(name = "lesson_requirement") public class LessonRequirementContainer extends AuditableEntity implements AuthorizationBatchEntity { // все прочее @Convert(converter = LessonRequirementJsonConverter.class) private LessonRequirement requirement; } // JSON конвертер public class LessonRequirementJsonConverter implements AttributeConverter { @Override public String convertToDatabaseColumn(@Valid LessonRequirement requirement) { // implement using Jackson } @Override @Valid public LessonRequirement convertToEntityAttribute(String databaseDataAsJSONString) { // implement using Jackson }