В этой заметке я хочу поговорить о проблеме, которая вставала перед большинством 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.
Комментариев нет:
Отправить комментарий