В этой заметке я хочу поговорить о проблеме, которая вставала перед большинством Java разработчиков задумывающихся об архитектуре ПО. Как можно понять из названия, это проблема анемичности классов сущностей JPA. Для тех кто не знаком с термином "Anemic Domain Model" сразу отсылаю к Martin Fowler. Коротко суть этого антипатерна в вырождении класса инкапсулирующего данные и логику в класс инкапсулирующий только данные. Таким образом подрывается одна из основ ООП: класс инкапсулирует данные и логику обработки этих данных. Вроде все просто и вывод напрашивается сам собой: поместить логику в класс сущностей JPA. Но стоп! Не все так просто. Попробую объяснить почему.
Чтобы сделать объяснение более конкретным, предлагаю в качестве примера гипотетическую систему учета товаров на складе. К системе предъявим следующие требования: приход товара (Goods) осуществляется по документу приходная накладная (ConsignmentNote), необходимо учитывать остатки товара на складе (Stock). Ограничусь двумя требованиями, чтобы не усложнять объяснение.
Классической реализацией сущностей JPA является класс с полями и методами доступа к ним (getter'ы и setter'ы). Реализуем 3 класса сущностей:
@Entity public class Goods { /** * Номеклатурный номер */ @Id private Long number; /** * Наименование */ private String name; //getter'ы и setter'ы } @Entity public class ConsignmentNote { /** * Номер документа */ @Id private Long number; /** * Дата документа */ private Date date; /** * Номенклатура товара */ private Goods goods; /** * Количество */ private BigDecimal quantity; /** * Цена */ private BigDecimal price; //getter'ы и setter'ы } @Entity public class Stock { /** * Номер */ @Id private Long number; /** * Дата, сутки в разрезе которых ведется учет */ private Date date; /** * Номенклатура товара */ private Goods goods; /** * Количество */ private BigDecimal quantity; /** * Цена */ private BigDecimal price; //getter'ы и setter'ы }
После реализации сущностей и встает главный вопрос: "Где поместить бизнес-логику". Допустим, необходимо пересчитать остатки за день по определенной номенклатуре товара. Реализуем соответствующую логику в классе Stock:
public void evaluate() { setQuantity(getPreviousStock().getQuantity()); for (ConsignmentNote consignmentNote: getGoods().getConsignmentNotes()) { if (getDate().equals(consignmentNote.getDate()) && getPrice().equals(consignmentNote.getPrice())) { setQuantity( getQuantity().add( consignmentNote.getQuantity() ) ); } } }
Код говорит сам за себя. Устанавливаем значение количества за предыдущую смену, просматриваем все документы по текущей номенклатуре и выбираем их все за дату и цену соответствующие установленным для объекта по которому идет расчет. Для этой реализации потребовалось добавить несколько дополнительных связей в классы сущностей. Ниже измененные классы:
@Entity public class Goods { //Уже реализованные поля /** * Все документы по текущей номенклатуре */ private List<consignmentnote> consignmentNotes; //getter'ы и setter'ы } @Entity public class Stock { //Уже реализованные поля /** * Остаток за предыдущую смену */ private Stock previousStock; /** * Остаток за следующую смену */ private Stock nextStock; //getter'ы и setter'ы }
В маленьком примере это не проблема заботиться о таких связях, а в реальном приложении их количество может быстро увеличиться и создать достаточно сложную сеть. Рассмотрим положительный момент нахождение бизнес логики в классе сущности. Допустим необходимо реализовать пересчет всех остатков по цепочке от определенной даты до настоящего времени. Сразу ясно, необходимо открыть код класса Stock и быстро реализовать новый метод:
public void evaluateAll() { evaluate(); if (getNextStock() != null) { getNextStock().evaluate(); } }
Всё ясно и понятно. Данные и код размещены в одном классе, внешне выглядит всё хорошо. Но где подвох? Он уже присутствует, но в тоже время могут появиться и новые проблемы. В текущей реализации проблема заключается в производительности метода evaluate. Данная реализация по сути осуществляет частичную выборку данных на стороне клиентского кода, тогда как это, по своей сути, задача СУБД. Изменить это очень трудно, а в некоторых случаях просто невозможно. Проблема кроется в мапировании связей сущностей, на мой взгляд, спорной стороне JPA. Новая проблема появляется, когда начинается работа со многими сущностями. Как реализовать расчет остатков за смену по некоторому заданному множеству номенклатур. С первого взгляда даже не понятно где должна располагаться логика данного функционала. Конечно можно посмотреть на него как на сервисный код, а не код бизнес-логики. И следовательно реализовать его в виде классов сервисов. Собственно данный подход требуется в показанной выше реализации. Всё ещё нужно выполнять логику выборки объектов над которыми производятся вычисления.
Выводы
Выводы
Не могу сказать, что реализация бизнес-логики в классах сущностей проигрышный вариант. И также не могу сказать обратное. Собственно, как и не существует серебряной пули для любой реальной технологии, так её нет и здесь. Из минусов объединения бизнес-логики и сущностей JPA могу отметить: большое число связей; трудность оптимальной реализации с точки зрения производительности; проблемы с определением места расположения кода бизнес-логики в неоднозначных ситуация. Из плюсов это расположение кода и данных в одном классе, что позволяет легко ориентироваться в коде и изменять его. В подходе с анемичными сущностями и реализации классов сервисов, в которых находится бизнес-логика ситуация прямо противоположная. Не зря он считается стандартным при реализации с помощью технологий Java EE.
Комментариев нет:
Отправить комментарий