Как я могу разрешить конфликт между слабой связью / внедрением зависимостей и моделью расширенного домена? - PullRequest
16 голосов
/ 29 марта 2009

Редактировать: Это не конфликт на теоретическом уровне, а конфликт на уровне реализации.

Другое Редактирование: Проблема не в том, что доменные модели являются только данными / DTO по сравнению с более сложной, более сложной картой объектов, в которой у Order есть элементы OrderItems и некоторая логика Calculate. Конкретная проблема заключается в том, что, например, заказ должен получить последние оптовые цены OrderItem из какого-либо веб-сервиса в Китае (например). Итак, у вас запущен Spring Service, который разрешает звонки на этот сервис PriceQuery в Китае. В ордере есть метод calcTotal, который перебирает каждый элемент OrderItem, получает последнюю цену и добавляет ее к общей сумме.

Итак, как бы вы обеспечили, чтобы в каждом Заказе была ссылка на эту услугу PriceQuery? Как бы вы восстановили его после десериализации, загрузки из БД и новых экземпляров? Это мой точный вопрос.

Самый простой способ - передать ссылку на метод calcTotal, но что, если ваш объект использует этот сервис внутренне на протяжении всего своего жизненного цикла? Что если он используется в 10 методах? Бессмысленно передавать ссылки каждый раз.

Другим способом было бы перевести CalcualTotal из Order в Сервис OrderService, но это нарушает дизайн ОО, и мы движемся к старому «сценарию транзакций».

Оригинальный пост:

Короткая версия: Богатые доменные объекты требуют ссылки на многие компоненты, но эти объекты сохраняются или сериализуются, поэтому любые ссылки, которые они хранят на внешние компоненты (в данном случае Spring-компоненты: службы, репозитории, что угодно), являются временными и уничтожаются. Их нужно повторно вводить, когда десериализованный объект загружается из БД, но это крайне уродливо, и я не вижу элегантного способа сделать это.

Более длинная версия: Некоторое время я практиковал слабую связь и DI с помощью Spring. Это очень помогло мне в управлении и проверке. Однако некоторое время назад я читал доменно-управляемый дизайн и немного Мартина Фаулера. В результате я пытался преобразовать свои доменные модели из простых DTO (обычно это простые представления строки таблицы, просто данные без логики) в более богатую модель домена.

По мере того, как мой домен растет и принимает на себя новые обязанности, моим доменным объектам начинают требоваться некоторые компоненты (сервисы, репозитории, компоненты), которые есть у меня в контексте Spring. Это быстро превратилось в кошмар и одну из самых сложных частей перехода к богатому дизайну доменов.

В основном есть моменты, когда я вручную вставляю ссылку на контекст приложения в мой домен:

  • когда объект загружается из репозитория или другого ответственного объекта, поскольку ссылки на компоненты являются временными и, очевидно, не сохраняются
  • когда объект создается из Factory, поскольку во вновь созданном объекте отсутствуют ссылки на компоненты
  • когда десериализован объект в задании Quartz или в каком-либо другом месте, так как ссылки на переходные компоненты стираются

Во-первых, это ужасно, потому что я передаю объекту ссылку на контекст приложения и ожидаю, что он извлечет по имени ссылки на компоненты, которые ему нужны. Это не инъекция, это прямое вытягивание.

Во-вторых, это ужасный код, потому что во всех упомянутых местах мне нужна логика для внедрения appContext

В-третьих, это подвержено ошибкам, потому что я должен помнить, что нужно вводить во всех этих местах все эти объекты, что сложнее, чем кажется.

Должен быть лучший способ, и я надеюсь, что вы сможете пролить на него немного света.

Ответы [ 6 ]

4 голосов
/ 29 марта 2009

Рискну сказать, что существует множество оттенков серого между наличием «модели анемичного домена» и встраиванием всех ваших услуг в объекты вашего домена. И довольно часто, по крайней мере, в бизнес-областях и, по моему опыту, объект может быть не чем иным, как просто данными; например, всякий раз, когда операции, которые могут быть выполнены с этим конкретным объектом, зависят от множества других объектов и некоторого локализованного контекста, скажем, например, адрес.

В моем обзоре литературы, ориентированной на предметную область, в сети я обнаружил много смутных идей и работ, но я не смог найти правильный, нетривиальный пример того, где границы между методами и операциями должны ложь, и, более того, как реализовать это с помощью текущего стека технологий. Поэтому для целей этого ответа я приведу небольшой пример, иллюстрирующий мои пункты:

Рассмотрим вековой пример 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 и примера отправки электронного письма после подсчета общей суммы, я бы сделал различие между:

  1. тот факт, что письмо должно быть отправлено после расчета цены
  2. какая часть заказа должна быть отправлена? (это может также включать, скажем, шаблоны электронной почты)
  3. фактический способ отправки электронного письма

Кроме того, нужно задаться вопросом, является ли отправка электронной почты свойственной 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(...);
}

Вот что вы получаете с этим подходом:

  1. доменные объекты не содержат сервисов, они используют их
  2. доменные объекты отделены от фактической реализации сервиса (например, SMTP, отправка в отдельном потоке и т. Д.), По природе механизма интерфейса
  3. сервисные интерфейсы являются общими, могут использоваться повторно, но не знают ни о каких реальных объектах домена. Например, если заказ получает дополнительное поле, вам нужно изменить только класс Order.
  4. вы можете легко макетировать сервисы и легко тестировать доменные объекты
  5. вы можете легко протестировать реализацию реальных сервисов

Я не знаю, соответствует ли это стандартам некоторых гуру, но это практичный подход, который достаточно хорошо работает на практике.

2 голосов
/ 26 апреля 2009

Я нашел ответ, по крайней мере, для тех, кто использует Spring:

6.8.1. Использование AspectJ для вставки зависимостей в доменные объекты с Spring

2 голосов
/ 24 апреля 2009

Regardinig

Что делать, если ваш заказ необходимо отправить электронное письмо каждый раз, когда итог рассчитывается?

Я бы использовал события.
Если он имеет какое-то значение для вас, когда заказ вычисляет его общую сумму, позвольте ему вызвать событие как eventDispatcher.raiseEvent (new ComputedTotalEvent (this)).
Затем вы прослушиваете события такого типа и отзываете свой заказ, как было сказано выше, чтобы он смог отформатировать шаблон электронной почты, и отправляете его.
Ваши доменные объекты остаются скудными, не зная об этом вашем требовании.
Короче говоря, разделите вашу проблему на 2 требования:
- Я хочу знать, когда заказ вычисляет его общую сумму;
- Я хочу отправить электронное письмо, когда заказ имеет (новый и другой) итог;

1 голос
/ 08 апреля 2009

Самый простой подход, который я могу придумать, - это добавить некоторую логику в уровень доступа к данным, который внедрит объект домена с его зависимостями перед возвратом на более высокий уровень (обычно называемый уровнем обслуживания). Вы можете аннотировать свойства каждого класса, чтобы указать, что нужно подключить. Если вы не используете Java 5+, вы можете реализовать интерфейс для каждого компонента, который необходимо внедрить, или даже объявить все это в XML и передать эти данные в контекст, который будет выполнять подключение. Если вы хотите получить фантазию, вы можете вытянуть это в аспект и применить его глобально на уровне доступа к данным, чтобы все методы, которые вытягивают доменные объекты, связывали их сразу после их возврата.

0 голосов
/ 29 марта 2009

Шаблон Identity Map может помочь в вашем сценарии. Посмотрите статью «Практические модели» , написанную Джереми Миллером, где он обсуждает эту модель.

0 голосов
/ 29 марта 2009

Возможно, вам нужен тип эталонного объекта, который будет сериализован как глобальная ссылка (например, URI) и сможет воскресать как прокси-сервер при десериализации в другом месте.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...