Рубрики
Без рубрики

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

Узнайте, как сохранить агрегаты DDD весной.

Автор оригинала: Mike Wojtyna.

1. Обзор

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

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

Агрегат – это группа бизнес-объектов, которые всегда должны быть . Таким образом, мы экономим и обновляем агрегаты в целом внутри транзакции.

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

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

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

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

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

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

Эти классы образуют простую совокупную . Оба orderLines и totalCost поля Заказ должны быть всегда последовательными, то есть totalCost всегда должны иметь значение, равное сумме всех orderLines .

Теперь у всех нас может возникнуть соблазн превратить все это в полноценную Java Beans. Но, обратите внимание, что введение простых getters и сеттеров в Заказ может легко сломать инкапсуляцию нашей модели и нарушить бизнес-ограничения.

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

2.2. Наивный агрегатный дизайн

Давайте представим, что могло бы произойти, если бы мы решили наивно добавить getters и сеттеров для всех свойств на Заказ класса, включая наборOrderTotal .

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

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

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

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

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

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

Корень в том, что заботится о всех наших бизнес- .

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

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;
    }

    // ...
}

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

Как мы видим, это довольно простой агрегат.

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

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

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

3. JPA и Hibernate

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


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

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

Вероятно, самой большой проблемой при работе с рамками ORM является упрощение нашей модели . Это также иногда называют Несоответствие объектно-реляционного . Давайте подумаем о том, что произойдет, если мы хотим сохранить нашу Заказ совокупность:

@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: Неизвестная сущность: com.baeldung.ddd.order.order . Очевидно, что нам не хватает некоторых требований JPA:

  1. Добавить отображение аннотаций
  2. ЗаказЛайн и Продукт классы должны быть сущностями или @Embeddable классы, а не простые объекты значения
  3. Добавьте пустой конструктор для каждого объекта или @Embeddable класс
  4. Замена Деньги свойства с простыми типами

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

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

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

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

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

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

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

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

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

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

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

Например, при работе с нашими Заказ совокупности, мы будем сталкиваться с трудностями, сохраняя Джода Деньги Поля.

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

Кроме того, мы также можем разделить Деньги собственности на два основных свойства. Например, Струнные для валютной единицы и БигДекимальные для фактической стоимости.

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

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

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

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

В принципе, у нас есть три варианта здесь:

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

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

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

4. Магазин документов

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

Для нужд этого учебника мы сосредоточимся на документах, похожих на JSON.

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

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

Теперь, Есть довольно много баз данных, которые могут хранить данные JSON, один из популярных время MongoDB. MongoDB фактически хранит BSON, или JSON в двоичной форме.

Благодаря MongoDB мы можем хранить порядок пример совокупности как есть .

Прежде чем двигаться дальше, давайте добавим весеннюю загрузку МонгоДБ стартер:


    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());
}

Что важно – мы не изменили первоначальный Заказ совокупные классы вообще; нет необходимости создавать конструкторы по умолчанию, сеттеры или пользовательский преобразователь для Деньги класс.

И вот что мы Заказ агрегат появляется в магазине:

{
  "_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.baeldung.ddd.order.mongo.Order"
}

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

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

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

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

Это абсолютно не означает, что MongoDB превосходит традиционные базы данных. Есть много законных случаев, в которых мы даже не должны пытаться моделировать наши классы как агрегаты и использовать базу данных S’L вместо этого.

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

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

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

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

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

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