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

Организация слоев с использованием шестиугольной архитектуры, DDD и Spring

Узнайте, как организовать слои приложений с помощью шестиугольной архитектуры, DDD и Spring.

Автор оригинала: Łukasz Ryś.

1. Обзор

В этом уроке мы реализуем приложение Spring с использованием DDD. Кроме того, мы организуем слои с помощью шестиугольной архитектуры.

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

2. Шестиугольная Архитектура

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

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

3. Принципы

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

Вместо этого мы разделим наше приложение на три уровня: приложение (снаружи), домен (внутри) и инфраструктура (снаружи):

Через уровень приложения пользователь или любая другая программа взаимодействует с приложением. Эта область должна содержать такие вещи, как пользовательские интерфейсы, контроллеры RESTful и библиотеки сериализации JSON. Он включает в себя все , что предоставляет доступ к нашему приложению и организует выполнение логики домена.

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

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

4. Доменный Уровень

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

Во-первых, мы должны создать класс Order :

public class Order {
    private UUID id;
    private OrderStatus status;
    private List orderItems;
    private BigDecimal price;

    public Order(UUID id, Product product) {
        this.id = id;
        this.orderItems = new ArrayList<>(Arrays.astList(new OrderItem(product)));
        this.status = OrderStatus.CREATED;
        this.price = product.getPrice();
    }

    public void complete() {
        validateState();
        this.status = OrderStatus.COMPLETED;
    }

    public void addOrder(Product product) {
        validateState();
        validateProduct(product);
        orderItems.add(new OrderItem(product));
        price = price.add(product.getPrice());
    }

    public void removeOrder(UUID id) {
        validateState();
        final OrderItem orderItem = getOrderItem(id);
        orderItems.remove(orderItem);

        price = price.subtract(orderItem.getPrice());
    }

    // getters
}

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

  • Заказ может быть создан только с заданным идентификатором и на основе одного Продукта – сам конструктор также вводит заказ с СОЗДАННЫМ статусом
  • После завершения заказа изменение Элемента заказа s невозможно
  • Невозможно изменить Порядок из – за пределов объекта домена, как с помощью сеттера

Кроме того, класс Order также отвечает за создание своего Элемента заказа .

Давайте создадим класс OrderItem , а затем:

public class OrderItem {
    private UUID productId;
    private BigDecimal price;

    public OrderItem(Product product) {
        this.productId = product.getId();
        this.price = product.getPrice();
    }

    // getters
}

Как мы видим, Элемент заказа создается на основе Продукта . Он сохраняет ссылку на него и сохраняет текущую цену Продукта .

Далее мы создадим интерфейс репозитория (a port в шестиугольной архитектуре). Реализация интерфейса будет осуществляться на уровне инфраструктуры:

public interface OrderRepository {
    Optional findById(UUID id);

    void save(Order order);
}

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

public class DomainOrderService implements OrderService {

    private final OrderRepository orderRepository;

    public DomainOrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public UUID createOrder(Product product) {
        Order order = new Order(UUID.randomUUID(), product);
        orderRepository.save(order);

        return order.getId();
    }

    @Override
    public void addProduct(UUID id, Product product) {
        Order order = getOrder(id);
        order.addOrder(product);

        orderRepository.save(order);
    }

    @Override
    public void completeOrder(UUID id) {
        Order order = getOrder(id);
        order.complete();

        orderRepository.save(order);
    }

    @Override
    public void deleteProduct(UUID id, UUID productId) {
        Order order = getOrder(id);
        order.removeOrder(productId);

        orderRepository.save(order);
    }

    private Order getOrder(UUID id) {
        return orderRepository
          .findById(id)
          .orElseThrow(RuntimeException::new);
    }
}

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

Поскольку доменный уровень полностью отделен от прикладного и инфраструктурного уровней, мы | можем также протестировать его независимо :

class DomainOrderServiceUnitTest {

    private OrderRepository orderRepository;
    private DomainOrderService tested;
    @BeforeEach
    void setUp() {
        orderRepository = mock(OrderRepository.class);
        tested = new DomainOrderService(orderRepository);
    }

    @Test
    void shouldCreateOrder_thenSaveIt() {
        final Product product = new Product(UUID.randomUUID(), BigDecimal.TEN, "productName");

        final UUID id = tested.createOrder(product);

        verify(orderRepository).save(any(Order.class));
        assertNotNull(id);
    }
}

5. Прикладной уровень

В этом разделе мы реализуем уровень приложений. Мы позволим пользователю взаимодействовать с вашим приложением через RESTful API.

Поэтому давайте создадим контроллер Order:

@RestController
@RequestMapping("/orders")
public class OrderController {

    private OrderService orderService;

    @Autowired
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    CreateOrderResponse createOrder(@RequestBody CreateOrderRequest request) {
        UUID id = orderService.createOrder(request.getProduct());

        return new CreateOrderResponse(id);
    }

    @PostMapping(value = "/{id}/products")
    void addProduct(@PathVariable UUID id, @RequestBody AddProductRequest request) {
        orderService.addProduct(id, request.getProduct());
    }

    @DeleteMapping(value = "/{id}/products")
    void deleteProduct(@PathVariable UUID id, @RequestParam UUID productId) {
        orderService.deleteProduct(id, productId);
    }

    @PostMapping("/{id}/complete")
    void completeOrder(@PathVariable UUID id) {
        orderService.completeOrder(id);
    }
}

Этот простой контроллер Spring Rest отвечает за организацию выполнения логики домена .

Этот контроллер адаптирует внешний интерфейс RESTful к нашему домену. Он делает это, вызывая соответствующие методы из Order Service (port).

6. Уровень Инфраструктуры

Уровень инфраструктуры содержит логику, необходимую для запуска приложения.

Поэтому мы начнем с создания классов конфигурации. Во-первых, давайте реализуем класс, который зарегистрирует наш сервис Order как Spring bean:

@Configuration
public class BeanConfiguration {

    @Bean
    OrderService orderService(OrderRepository orderRepository) {
        return new DomainOrderService(orderRepository);
    }
}

Затем давайте создадим конфигурацию, ответственную за включение хранилищ данных Spring, которые мы будем использовать:

@EnableMongoRepositories(basePackageClasses = SpringDataMongoOrderRepository.class)
public class MongoDBConfiguration {
}

Мы использовали свойство basePackageClasses , потому что эти репозитории могут находиться только на уровне инфраструктуры. Следовательно, у Spring нет причин сканировать все приложение целиком. Кроме того, этот класс может содержать все, что связано с установлением соединения между MongoDB и нашим приложением.

Наконец, мы реализуем OrderRepository с уровня домена. Мы будем использовать наш Spring Data Mongo OrderRepository в нашей реализации:

@Component
public class MongoDbOrderRepository implements OrderRepository {

    private SpringDataMongoOrderRepository orderRepository;

    @Autowired
    public MongoDbOrderRepository(SpringDataMongoOrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public Optional findById(UUID id) {
        return orderRepository.findById(id);
    }

    @Override
    public void save(Order order) {
        orderRepository.save(order);
    }
}

Эта реализация хранит наш Заказ в MongoDB. В шестиугольной архитектуре эта реализация также является адаптером.

7. Преимущества

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

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

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

На самом деле, давайте изменим уровень инфраструктуры, чтобы использовать Cassandra в качестве базы данных:

@Component
public class CassandraDbOrderRepository implements OrderRepository {

    private final SpringDataCassandraOrderRepository orderRepository;

    @Autowired
    public CassandraDbOrderRepository(SpringDataCassandraOrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public Optional findById(UUID id) {
        Optional orderEntity = orderRepository.findById(id);
        if (orderEntity.isPresent()) {
            return Optional.of(orderEntity.get()
                .toOrder());
        } else {
            return Optional.empty();
        }
    }

    @Override
    public void save(Order order) {
        orderRepository.save(new OrderEntity(order));
    }

}

В отличие от MongoDB, теперь мы используем объект Order для сохранения домена в базе данных.

Если мы добавим технологические аннотации к нашему объекту Order domain , то мы нарушим разделение между уровнями инфраструктуры и домена .

Репозиторий адаптирует домен к нашим потребностям в постоянстве.

Давайте сделаем еще один шаг вперед и превратим наше приложение RESTful в приложение командной строки:

@Component
public class CliOrderController {

    private static final Logger LOG = LoggerFactory.getLogger(CliOrderController.class);

    private final OrderService orderService;

    @Autowired
    public CliOrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    public void createCompleteOrder() {
        LOG.info("<>");
        UUID orderId = createOrder();
        orderService.completeOrder(orderId);
    }

    public void createIncompleteOrder() {
        LOG.info("<>");
        UUID orderId = createOrder();
    }

    private UUID createOrder() {
        LOG.info("Placing a new order with two products");
        Product mobilePhone = new Product(UUID.randomUUID(), BigDecimal.valueOf(200), "mobile");
        Product razor = new Product(UUID.randomUUID(), BigDecimal.valueOf(50), "razor");
        LOG.info("Creating order with mobile phone");
        UUID orderId = orderService.createOrder(mobilePhone);
        LOG.info("Adding a razor to the order");
        orderService.addProduct(orderId, razor);
        return orderId;
    }
}

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

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

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

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

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

Затем мы придумали реализацию для каждого слоя:

Наконец, мы поменяли местами уровни приложений и инфраструктуры, не затрагивая домен.

Как всегда, код для этих примеров доступен на GitHub .