Google Analytics

воскресенье, 6 марта 2011 г.

Anemic Domain Model и JPA сущности

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

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

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