Статьи

Богатая доменная модель с Guice

Модель анемичной области является действительно распространенным анти-паттерном. В мире фреймворков ORM & DI мы, естественно, находимся в управляемом ORM «домене», который представляет собой все данные, а не поведение; в сочетании с вспомогательными классами, которые являются поведением и не содержат данных, полезными для внедрения в нашу структуру DI.

В этой статье я рассмотрю один из возможных подходов к реализации модели с расширенным доменом — в этом примере используется Guice , хотя я уверен, что у Spring и т. Д. Будут разные способы достижения одной и той же цели.

Эта проблема

Весь исходный код можно найти на github . Ветвь «master» показывает оригинальный, плохо разложенный код. Ветвь «rich-domain» показывает решение, которое я описываю.

Модель анемичной области

Во-первых, наша анемичная модель домена — TradeOrder.java . Этот класс, как это принято в Hibernate, содержит множество аннотаций, описывающих модель данных, поля для всех данных, методы доступа и мутаторы для доступа к данным, и ничего более не интересного. Я предполагаю, что в этой области TradeOrders делает вещи. Возможно мы разместим заказ или отменим заказ. Где-то вдоль линии ключевые объекты в нашем домене, вероятно, должны иметь некоторое поведение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entity
@Table(name="TRADE_ORDER")
public class TradeOrder {
    @Id
    @Column(name="ID", length=32)
    @GeneratedValue
    private String id;
 
    @ManyToOne
    @JoinColumn(name="CURRENCY_ID", nullable=false)
    @ForeignKey(name="FK_ORDER_CURRENCY")
    @AccessType("field")
    private Currency currency;
 
    @Column(name="AMOUNT", nullable=true)
    private BigDecimal amount;
 
    public TradeOrder() { }
 
    public String getId() { return id; }
 
    public Currency getCurrency() { return currency; }
    public void setCurrency(Currency currency) { this.currency = currency; }
 
    public BigDecimal getAmount() { return amount; }
    public void setAmount(BigDecimal amount) { this.amount = amount; }
}

Хелпер класс

В этом действительно простом примере нам нужно выяснить стоимость ордера — то есть количество акций, которые мы хотим купить (или продать), и цену за акцию, которую мы платим. К сожалению, поскольку это включает в себя зависимости, поведение которых не находится в классе, к которому оно относится, вместо этого оно было перемещено в очень полезный вспомогательный класс.

Взгляните на FiguresFactory.java . Этот класс имеет только один публичный метод — buildFrom. Целью этого метода является создание фигур из TradeOrder.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public Figures buildFrom(TradeOrder order, Date effectiveDate)
 throws OrderProcessingException {
    Date tradeDate = order.getTradeDate();
    HedgeFundAsset asset = order.getAsset();
 
    BigDecimal bestPrice = bestPriceFor(asset, tradeDate);
 
    return order.getType() == TradeOrderType.REDEMPTION
        ? figuresFromPosition(
              order,
              lookupPosition(asset, order.getFohf(), tradeDate),
              lookupPosition(asset, order.getFohf(), effectiveDate),
              bestPrice)
        : getFigures(order, bestPrice, null);
}

Помимо «даты вступления в силу» (какой бы она ни была), единственным способом, который этот метод использует, является TradeOrder. Используя обильное количество получателей в TradeOrder, он запрашивает данные для обработки, вместо того, чтобы сообщать TradeOrder, что ему нужно. В идеальной объектно-ориентированной системе это был бы метод в TradeOrder, называемый createFigures.

Почему мы оказались здесь? Во всем виновата структура внедрения зависимостей! Поскольку процесс создания объекта Figures требует от нас разрешения цен и валют, нам необходимо просмотреть эти данные, используя инъекционные зависимости. В нашем анемичном домене не могут быть вставлены зависимости, поэтому вместо этого мы внедряем их в этот маленький вспомогательный класс.

В итоге мы получаем классическую модель анемичной области. TradeOrder имеет данные; в то время как многочисленные вспомогательные классы, такие как FiguresFactory, содержат поведение, которое работает с этими данными. Это все очень не OO.

Лучший способ

Запись данных

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

Чтобы упростить конструирование этих объектов записи, я использовал библиотеку karg, написанную моим коллегой — для этого нам необходимо объявить набор аргументов, с помощью которых мы можем создать запись, и конструктор, принимающий список аргументов. , Это значительно упрощает конструкцию и позволяет избежать использования конструктора, который принимает 27 строк (например).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entity
@Table(name="TRADE_ORDER")
public class TradeOrderRecord {
    @Id
    @Column(name="ID", length=32)
    @GeneratedValue
    public String id;
 
    @Column(name="CURRENCY_ID")
    public String currencyId;
 
    @Column(name="AMOUNT", nullable=true)
    public BigDecimal amount;
 
    public static class Arguments {
     public static final Keyword<String> CURRENCY_ID = newKeyword();
     public static final Keyword<BigDecimal> AMOUNT = newKeyword();
    }
 
    protected TradeOrderRecord() { }
 
    public TradeOrderRecord(KeywordArguments arguments) {
     this.currencyId = Arguments.CURRENCY_ID.from(arguments);
     this.amount = Arguments.AMOUNT.from(arguments);
    }
}

Богатый домен

Наша цель — сделать TradeOrder богатым доменным объектом — он должен иметь все поведение и данные, связанные с концепцией домена «TradeOrder».

Данные

Первое, что понадобится TradeOrder, — это внутреннее хранение всех данных, связанных с TradeOrder (по крайней мере, в качестве отправной точки, неиспользуемые поля намекают на то, что мы могли бы еще больше упростить это).

1
2
3
4
public class TradeOrder {
    private final String id;
    private final String currencyId;
    private final BigDecimal amount;

Мы делаем данные неизменяемыми . Неизменяемое состояние, как правило, является хорошей вещью, и здесь оно заставляет нас понять, что это полностью заполненный TradeOrder, и, поскольку он имеет идентификатор, он всегда связан со строкой в ​​базе данных. Делая TradeOrder неизменным, очевидный вопрос — как мне обновить заказ? Ну, есть множество способов сделать это, но это другая история для другого времени.

Нам тоже не нужны аксессоры. Поскольку мы планируем поместить все поведение, относящееся к TradeOrder, к самому классу TradeOrder, другим классам не нужно запрашивать информацию, им нужно только сообщать нам, чего они хотят достичь.

Примечание: есть один (в настоящее время устаревший) метод доступа, который намекает на дальнейшее поведение, которое следует переместить.

зависимости

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

1
2
3
4
private final CurrencyCache currencyCache;
private final PriceFetcher bestPriceFetcher;
private final PositionFetcher hedgeFundAssetPositionsFetcher;
private final FXService fxService;

Некоторые люди сочтут это оскорбительным — смешивать зависимости с данными. Тем не менее, лично я думаю, что компромисс стоит того — польза от возможности определять наше поведение в классе, к которому они относятся, стоит того.

Поведение

Теперь у нас есть данные и зависимости в одном месте, относительно легко перемещаться по методам из FiguresFactory:

01
02
03
04
05
06
07
08
09
10
public Figures createFigures(Date effectiveDate) throws OrderProcessingException {
    BigDecimal bestPrice = bestPriceFor(this.asset, this.tradeDate);
 
    return this.type == TradeOrderType.REDEMPTION
        ? figuresFromPosition(
              fohf,
              lookupPosition(this.asset, fohf, this.tradeDate),
              lookupPosition(this.asset, fohf, effectiveDate), bestPrice)
        : getFigures(fohf, bestPrice, null);
}

строительство

Последнее, что нам нужно решить, — это как создавать экземпляры TradeOrder. Поскольку все поля данных и зависимостей помечены как окончательные, конструктор должен инициализировать их все. Это означает, что нам нужен конструктор, который принимает зависимости и TradeOrderRecord (т.е. объект значения, который мы читаем из базы данных):

1
2
3
4
5
6
7
8
@Inject
protected TradeOrder(CurrencyCache currencyCache,
                     PriceFetcher bestPriceFetcher,
                     PositionFetcher hedgeFundAssetPositionsFetcher,
                     FXService fxService,
                     @Assisted TradeOrderRecord record) {
    ...
}

Это не очень красиво, но главное, на что нужно обратить внимание, это аннотация @Assisted. Это позволяет нам сказать Guice, что другие зависимости вводятся нормально, тогда как TradeOrderRecord должен передаваться из фабричного метода. Сам фабричный интерфейс выглядит так:

1
2
3
public static interface Factory {
 TradeOrder create(TradeOrderRecord record);
}

Нам не нужно реализовывать этот интерфейс, Guice предоставляет его автоматически. TradeOrder.Factory становится зависимостью для инъекций, которую мы можем использовать откуда угодно, когда нам нужно создать экземпляр TradeOrder. Guice инициализирует вводимые зависимости как обычно, и вспомогательная зависимость — TradeOrderRecord — передается с фабрики. Поэтому нашему вызывающему коду не нужно беспокоиться о том, что нашему богатому домену нужны инъекционные зависимости.

1
2
3
4
@Inject private TradeOrder.Factory tradeOrderFactory;
...
TradeOrderRecord record = tradeOrderDAO.loadById(id);
TradeOrder order = tradeOrderFactory.create(record);

Вывод

Объединяя зависимости и данные вместе в богатую модель предметной области, мы можем определить класс с правильным поведением. Очевидный запах кода в TradeOrder сейчас заключается в том, что детальная механика создания фигур, вероятно, является отдельной задачей и должна быть устранена. Это нормально, мы можем ввести новую зависимость для управления этим — до тех пор, пока TradeOrder все еще является отправной точкой для создания объекта Figures.

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

Ссылка: Реализация богатого домена м Оделись с Guice от нашего партнера JCG Дэвида Грина в Actively Lazy Blog .

Статьи по Теме :