Сохраняющиеся агрегаты DDD

Сохраняющиеся агрегаты DDD

1. обзор

В этом руководстве мы рассмотрим возможности сохраненияDDD Aggregates с помощью различных технологий.

2. Введение в агрегаты

An aggregate is a group of business objects which always need to be consistent. Поэтому мы сохраняем и обновляем агрегаты в целом внутри транзакции.

Агрегат является важной тактической моделью в DDD, которая помогает поддерживать согласованность наших бизнес-объектов. Однако идея агрегата также полезна вне контекста DDD.

Есть множество бизнес-случаев, когда этот шаблон может пригодиться. As a rule of thumb, we should consider using aggregates when there are multiple objects changed as part of the same transaction.

Давайте посмотрим, как мы можем применить это при моделировании заказа на покупку.

2.1. Пример заказа на покупку

Итак, предположим, мы хотим смоделировать заказ на покупку:

class Order {
    private Collection orderLines;
    private Money totalCost;
    // ...
}
class OrderLine {
    private Product product;
    private int quantity;
    // ...
}
class Product {
    private Money price;
    // ...
}

These classes form a simple aggregate. Оба поляorderLines иtotalCostOrder должны быть всегда согласованными, то естьtotalCost всегда должен иметь значение, равное сумме всехorderLines.

Now, we all might be tempted to turn all of these into fully-fledged Java Beans. Но обратите внимание, что введение простых геттеров и сеттеров вOrder может легко нарушить инкапсуляцию нашей модели и нарушить бизнес-ограничения.

Посмотрим, что может пойти не так.

2.2. Наивный Совокупный Дизайн

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

Ничто не мешает нам выполнить следующий код:

Order order = new Order();
order.setOrderLines(Arrays.asList(orderLine0, orderLine1));
order.setTotalCost(Money.zero(CurrencyUnit.USD)); // this doesn't look good...

В этом коде мы вручную устанавливаем свойствоtotalCost на ноль, нарушая важное бизнес-правило. Определенно, общая стоимость не должна быть равна нулю долларов!

Нам нужен способ защитить наши бизнес-правила. Давайте посмотрим, как могут помочь агрегированные корни.

2.3. Совокупный корень

aggregate root - это класс, который работает как точка входа в нашу совокупность. All business operations should go through the root. Таким образом, корень агрегата может заботиться о поддержании агрегата в согласованном состоянии.

The root is what takes cares of all our business invariants.

И в нашем примере классOrder является правильным кандидатом на роль агрегированного корня. Нам просто нужно внести некоторые изменения, чтобы гарантировать, что агрегат всегда согласован:

class Order {
    private final List orderLines;
    private Money totalCost;

    Order(List orderLines) {
        checkNotNull(orderLines);
        if (orderLines.isEmpty()) {
            throw new IllegalArgumentException("Order must have at least one order line item");
        }
        this.orderLines = new ArrayList<>(orderLines);
        totalCost = calculateTotalCost();
    }

    void addLineItem(OrderLine orderLine) {
        checkNotNull(orderLine);
        orderLines.add(orderLine);
        totalCost = totalCost.plus(orderLine.cost());
    }

    void removeLineItem(int line) {
        OrderLine removedLine = orderLines.remove(line);
        totalCost = totalCost.minus(removedLine.cost());
    }

    Money totalCost() {
        return totalCost;
    }

    // ...
}

Использование агрегированного корня теперь позволяет нам более легко превращатьProduct иOrderLine в неизменяемые объекты, где все свойства являются окончательными.

Как мы видим, это довольно простая совокупность.

И мы могли бы просто каждый раз рассчитывать общую стоимость без использования поля.

Однако сейчас мы говорим только о сохранении совокупности, а не о совокупном дизайне. Оставайтесь с нами, так как этот конкретный домен пригодится через мгновение.

Насколько хорошо это играет с постоянными технологиями? Давайте взглянем. Ultimately, this will help us to choose the right persistence tool for our next project.

3. JPA и Hibernate

В этом разделе давайте попробуем сохранить агрегатOrder с помощью JPA и Hibernate. Мы будем использовать Spring Boot и стартерJPA:


    org.springframework.boot
    spring-boot-starter-data-jpa

Для большинства из нас это, кажется, самый естественный выбор. В конце концов, мы годами работали с реляционными системами, и все мы знаем популярные ORM-фреймворки.

Probably the biggest problem when working with ORM frameworks is the simplification of our model design. Его также иногда называютObject-relational impedance mismatch. Давайте подумаем, что бы произошло, если бы мы хотели сохранить агрегатOrder:

@DisplayName("given order with two line items, when persist, then order is saved")
@Test
public void test() throws Exception {
    // given
    JpaOrder order = prepareTestOrderWithTwoLineItems();

    // when
    JpaOrder savedOrder = repository.save(order);

    // then
    JpaOrder foundOrder = repository.findById(savedOrder.getId())
      .get();
    assertThat(foundOrder.getOrderLines()).hasSize(2);
}

На этом этапе этот тест вызовет исключение:java.lang.IllegalArgumentException: Unknown entity: com.example.ddd.order.Order. Obviously, we’re missing some of the JPA requirements:

  1. Добавить аннотации сопоставления

  2. КлассыOrderLine иProduct должны быть сущностями или классами@Embeddable, а не простыми объектами значений

  3. Добавьте пустой конструктор для каждой сущности или класса@Embeddable

  4. Замените свойстваMoney простыми типами

Хм, нам нужно изменить структуру агрегатаOrder, чтобы иметь возможность использовать JPA. Хотя добавление аннотаций не представляет большого труда, другие требования могут вызвать множество проблем.

3.1. Изменения в Объектах Значения

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

We need to add artificial ids to the OrderLine and Product, even if these classes were never designed to have identifiers. Мы хотели, чтобы они были простыми ценностными объектами.

Вместо этого можно использовать аннотации@Embedded и@ElementCollection, но этот подход может значительно усложнить ситуацию при использовании графа сложных объектов (например, объект@Embeddable, имеющий другое свойство@Embedded и т. Д. .).

Использование аннотации@Embedded просто добавляет плоские свойства к родительской таблице. Кроме этого, основные свойства (например, типаString) по-прежнему требуется метод установки, который нарушает конструкцию объекта желаемого значения.

Empty constructor requirement forces the value object properties to not be final anymore, breaking an important aspect of our original design. По правде говоря, Hibernate может использовать частный конструктор без аргументов, который немного смягчает проблему, но он все еще далек от совершенства.

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

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

3.2. Сложные типы

К сожалению, мы не можем ожидать, что JPA автоматически отобразит сторонние сложные типы в таблицы. Вы только посмотрите, сколько изменений мы должны были внести в предыдущем разделе!

Например, при работе с нашим агрегатомOrder мы столкнемся с трудностями при сохранении полейJoda Money.

В таком случае мы можем написать собственный тип@Converter, доступный в JPA 2.1. Это может потребовать дополнительной работы, хотя.

В качестве альтернативы мы также можем разделить свойствоMoney на два основных свойства. Например,String для денежной единицы иBigDecimal для фактического значения.

Хотя мы можем скрыть детали реализации и по-прежнему использовать классMoney через API общедоступных методов, практика показывает, что большинство разработчиков не могут оправдать дополнительную работу и вместо этого просто выродят модель, чтобы она соответствовала спецификации JPA.

3.3. Заключение

Хотя JPA - одна из наиболее распространенных спецификаций в мире, она может быть не лучшим вариантом для сохранения агрегатаOrder.

Если мы хотим, чтобы наша модель отражала истинные бизнес-правила, мы должны разработать ее так, чтобы она не была простым представлением 1: 1 базовых таблиц.

По сути, у нас есть три варианта:

  1. Создайте набор простых классов данных и используйте их для сохранения и воссоздания богатой бизнес-модели. К сожалению, это может потребовать много дополнительной работы.

  2. Примите ограничения JPA и выберите правильный компромисс.

  3. Рассмотрим другую технологию.

Первый вариант имеет самый большой потенциал. На практике большинство проектов разрабатывается с использованием второго варианта.

Теперь давайте рассмотрим другую технологию для сохранения агрегатов.

4. Хранилище документов

Хранилище документов - это альтернативный способ хранения данных. Вместо использования отношений и таблиц мы сохраняем целые объекты. This makes a document store a potentially perfect candidate for persisting aggregates.

В этом руководстве мы сосредоточимся на документах, подобных JSON.

Давайте подробнее рассмотрим, как наша проблема с сохранением порядка выглядит в хранилище документов, таком как MongoDB.

4.1. Сохранение агрегирования с использованием MongoDB

Now, there are quite a few databases which can store JSON data, one of the popular being MongoDB. MongoDB фактически хранит BSON или JSON в двоичной форме.

Благодаря MongoDB мы можем хранить пример агрегатаOrderas-is.

Прежде чем мы продолжим, давайте добавим стартер Spring BootMongoDB:


    org.springframework.boot
    spring-boot-starter-data-mongodb

Теперь мы можем запустить аналогичный тестовый пример, как в примере с JPA, но на этот раз используя MongoDB:

@DisplayName("given order with two line items, when persist using mongo repository, then order is saved")
@Test
void test() throws Exception {
    // given
    Order order = prepareTestOrderWithTwoLineItems();

    // when
    repo.save(order);

    // then
    List foundOrders = repo.findAll();
    assertThat(foundOrders).hasSize(1);
    List foundOrderLines = foundOrders.iterator()
      .next()
      .getOrderLines();
    assertThat(foundOrderLines).hasSize(2);
    assertThat(foundOrderLines).containsOnlyElementsOf(order.getOrderLines());
}

What’s important – we didn’t change the original Order aggregate classes at all; no необходимо создать конструкторы по умолчанию, сеттеры или пользовательский преобразователь для классаMoney.

И вот что наш агрегатOrder появляется в магазине:

{
  "_id": ObjectId("5bd8535c81c04529f54acd14"),
  "orderLines": [
    {
      "product": {
        "price": {
          "money": {
            "currency": {
              "code": "USD",
              "numericCode": 840,
              "decimalPlaces": 2
            },
            "amount": "10.00"
          }
        }
      },
      "quantity": 2
    },
    {
      "product": {
        "price": {
          "money": {
            "currency": {
              "code": "USD",
              "numericCode": 840,
              "decimalPlaces": 2
            },
            "amount": "5.00"
          }
        }
      },
      "quantity": 10
    }
  ],
  "totalCost": {
    "money": {
      "currency": {
        "code": "USD",
        "numericCode": 840,
        "decimalPlaces": 2
      },
      "amount": "70.00"
    }
  },
  "_class": "com.example.ddd.order.mongo.Order"
}

Этот простой документ BSON содержит всю совокупностьOrder в одном куске, что хорошо согласуется с нашим первоначальным представлением о том, что все это должно быть согласовано вместе.

Обратите внимание, что сложные объекты в документе BSON просто сериализуются как набор обычных свойств JSON. Благодаря этому даже сторонние классы (например,Joda Money) можно легко сериализовать без необходимости упрощения модели.

4.2. Заключение

Сохранение агрегатов с использованием MongoDB проще, чем с использованием JPA.

This absolutely doesn’t mean MongoDB is superior to traditional databases. Существует множество законных случаев, когда нам даже не следует пытаться моделировать наши классы как агрегаты и вместо этого использовать базу данных SQL.

Тем не менее, когда мы определили группу объектов, которые должны всегда соответствовать сложным требованиям, использование хранилища документов может оказаться очень привлекательным вариантом.

5. Заключение

В DDD агрегаты обычно содержат самые сложные объекты в системе. Работа с ними требует совсем другого подхода, чем в большинстве приложений CRUD.

Использование популярных ORM-решений может привести к упрощенной или чрезмерно открытой доменной модели, которая часто не может выразить или применить сложные бизнес-правила.

Хранилища документов могут упростить сохранение агрегатов без ущерба для сложности модели.

Полный исходный код всех примеров доступенover on GitHub.