Рискну сказать, что существует множество оттенков серого между наличием «модели анемичного домена» и встраиванием всех ваших услуг в объекты вашего домена. И довольно часто, по крайней мере, в бизнес-областях и, по моему опыту, объект может быть не чем иным, как просто данными; например, всякий раз, когда операции, которые могут быть выполнены с этим конкретным объектом, зависят от множества других объектов и некоторого локализованного контекста, скажем, например, адрес.
В моем обзоре литературы, ориентированной на предметную область, в сети я обнаружил много смутных идей и работ, но я не смог найти правильный, нетривиальный пример того, где границы между методами и операциями должны ложь, и, более того, как реализовать это с помощью текущего стека технологий. Поэтому для целей этого ответа я приведу небольшой пример, иллюстрирующий мои пункты:
Рассмотрим вековой пример Orders и OrderItems. «Анемичная» модель предметной области будет выглядеть примерно так:
class Order {
Long orderId;
Date orderDate;
Long receivedById; // user which received the order
}
class OrderItem {
Long orderId; // order to which this item belongs
Long productId; // product id
BigDecimal amount;
BigDecimal price;
}
По моему мнению, смыслом доменного дизайна является использование классов для лучшего моделирования отношений между сущностями. Таким образом, неанемичная модель будет выглядеть примерно так:
class Order {
Long orderId;
Date orderDate;
User receivedBy;
Set<OrderItem> items;
}
class OrderItem {
Order order;
Product product;
BigDecimal amount;
BigDecimal price;
}
Предположительно, вы будете использовать решение ORM для отображения здесь. В этой модели вы сможете написать метод, такой как Order.calculateTotal()
, который суммирует все amount*price
для каждого элемента заказа.
Таким образом, модель будет насыщенной, в том смысле, что операции, имеющие смысл с точки зрения бизнеса, такие как calculateTotal
, будут размещены в объекте домена Order
. Но, по крайней мере, на мой взгляд, доменный дизайн не означает, что Order
должен знать о ваших сервисах персистентности. Это должно быть сделано в отдельном и независимом слое. Постоянные операции не являются частью бизнес-сферы, они являются частью реализации.
И даже в этом простом примере есть много подводных камней для рассмотрения. Должен ли весь Product
быть загружен с каждым OrderItem
? Если существует огромное количество элементов заказа, и вам нужен сводный отчет по огромному количеству заказов, будете ли вы использовать Java, загружать объекты в память и вызывать calculateTotal()
для каждого заказа? Или SQL-запрос является гораздо лучшим решением с любой стороны. Вот почему достойное решение ORM, такое как Hibernate, предлагает механизмы для решения именно таких практических задач: отложенная загрузка с прокси для первого и HQL для второго. Какая польза от теоретически обоснованной модели, если генерация отчетов занимает годы?
Конечно, весь вопрос довольно сложный, гораздо больше, что я могу написать или рассмотреть за один присест. И я говорю не с позиции авторитета, а с простой повседневной практикой развертывания бизнес-приложений. Надеюсь, вы получите что-то из этого ответа. Не стесняйтесь предоставить некоторые дополнительные детали и примеры того, с чем вы имеете дело ...
Редактировать : Что касается услуги PriceQuery
и примера отправки электронного письма после подсчета общей суммы, я бы сделал различие между:
- тот факт, что письмо должно быть отправлено после расчета цены
- какая часть заказа должна быть отправлена? (это может также включать, скажем, шаблоны электронной почты)
- фактический способ отправки электронного письма
Кроме того, нужно задаться вопросом, является ли отправка электронной почты свойственной Order
или еще одной вещью, которую можно сделать с ней, например, сохранение ее, сериализация в различные форматы (XML, CSV, Excel) и т. д.
Что бы я сделал, и что я считаю хорошим подходом ООП, так это следующее. Определите интерфейс, включающий операции подготовки и отправки электронного письма:
interface EmailSender {
public void setSubject(String subject);
public void addRecipient(String address, RecipientType type);
public void setMessageBody(String body);
public void send();
}
Теперь внутри класса Order
определите операцию, с помощью которой заказ «знает», как отправить себя как электронное письмо, используя отправителя электронной почты:
class Order {
...
public void sendTotalEmail(EmailSender sender) {
sender.setSubject("Order " + this.orderId);
sender.addRecipient(receivedBy.getEmailAddress(), RecipientType.TO);
sender.addRecipient(receivedBy.getSupervisor().getEmailAddress(), RecipientType.BCC);
sender.setMessageBody("Order total is: " + calculateTotal());
sender.send();
}
Наконец, у вас должен быть фасад к операциям вашего приложения, точка, где происходит фактический ответ на действия пользователя. По моему мнению, именно здесь вы должны получить (по Spring DI) фактические реализации сервисов. Это может быть, например, класс Spring MVC Controller
:
public class OrderEmailController extends BaseFormController {
// injected by Spring
private OrderManager orderManager; // persistence
private EmailSender emailSender; // actual sending of email
public ModelAndView processFormSubmission(HttpServletRequest request,
HttpServletResponse response, ...) {
String id = request.getParameter("id");
Order order = orderManager.getOrder(id);
order.sendTotalEmail(emailSender);
return new ModelAndView(...);
}
Вот что вы получаете с этим подходом:
- доменные объекты не содержат сервисов, они используют их
- доменные объекты отделены от фактической реализации сервиса (например, SMTP, отправка в отдельном потоке и т. Д.), По природе механизма интерфейса
- сервисные интерфейсы являются общими, могут использоваться повторно, но не знают ни о каких реальных объектах домена. Например, если заказ получает дополнительное поле, вам нужно изменить только класс
Order
.
- вы можете легко макетировать сервисы и легко тестировать доменные объекты
- вы можете легко протестировать реализацию реальных сервисов
Я не знаю, соответствует ли это стандартам некоторых гуру, но это практичный подход, который достаточно хорошо работает на практике.