1. 概要

このチュートリアルでは、DDDを使用してSpringアプリケーションを実装します。 さらに、ヘキサゴナルアーキテクチャを使用してレイヤーを整理します。

このアプローチでは、アプリケーションのさまざまなレイヤーを簡単に交換できます。

2. 六角形のアーキテクチャ

六角形のアーキテクチャはのモデルですドメインロジックを中心としたソフトウェアアプリケーションの設計外的要因からそれを分離します。

ドメインロジックはビジネスコアで指定されます。これを内部部分と呼び、残りは外部部分と呼びます。 外部からのドメインロジックへのアクセスは、 ポートアダプター 。 

3. 原則

まず、コードを分割するための原則を定義する必要があります。 すでに簡単に説明したように、六角形のアーキテクチャは内側と外側の部分を定義します。

代わりに、アプリケーションを3つのレイヤーに分割します。 アプリケーション(外部)、ドメイン(内部)、およびインフラストラクチャ(外部):

アプリケーション層を介して、ユーザーまたはその他のプログラムはアプリケーションと対話します。 この領域には、ユーザーインターフェイス、RESTfulコントローラー、JSONシリアル化ライブラリなどが含まれている必要があります。 これには、アプリケーションへのエントリを公開し、ドメインロジックの実行を調整するものが含まれます。

ドメインレイヤーでは、ビジネスロジックにアクセスして実装するコードを保持します。 これが私たちのアプリケーションの中核です。 さらに、このレイヤーは、アプリケーション部分とインフラストラクチャ部分の両方から分離する必要があります。 さらに、ドメインが相互作用するデータベースなどの外部部分と通信するためのAPIを定義するインターフェースも含まれている必要があります。

最後に、インフラストラクチャ層は、データベース構成やSpring構成など、アプリケーションが機能するために必要なすべてを含む部分です。 さらに、ドメイン層からインフラストラクチャに依存するインターフェイスも実装します。

4. ドメインレイヤー

ドメインレイヤーであるコアレイヤーを実装することから始めましょう。

まず、Orderクラスを作成する必要があります。

public class Order {
    private UUID id;
    private OrderStatus status;
    private List<OrderItem> 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 は、それ自体を正しい状態に保つ責任があります。

  • 注文は、指定されたIDでのみ作成でき、1つの製品に基づいています– コンストラクター自体も、CREATEDステータスで注文を開始します
  • 注文が完了すると、OrderItemを変更することはできません
  • セッターのように、ドメインオブジェクトの外部からOrderを変更することはできません。

さらに、 Order クラスは、そのOrderItemの作成も担当します。

次に、OrderItemクラスを作成しましょう。

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

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

    // getters
}

ご覧のとおり、OrderItemProductに基づいて作成されています。 それへの参照を保持し、製品の現在の価格を保存します。

次に、リポジトリインターフェイス(ヘキサゴナルアーキテクチャのポート)を作成します。 インターフェイスの実装は、インフラストラクチャ層で行われます。

public interface OrderRepository {
    Optional<Order> findById(UUID id);

    void save(Order 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 bean として登録しません。これは、ドメインの観点から、これは内側の部分にあり、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. アプリケーション層

このセクションでは、アプリケーション層を実装します。 ユーザーがRESTfulAPIを介してアプリケーションと通信できるようにします。

したがって、 OrderController:を作成しましょう。

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

この単純なSpringRestコントローラーは、ドメインロジックの実行を調整する役割を果たします。

このコントローラーは、外部のRESTfulインターフェースをドメインに適合させます。 OrderService (port)から適切なメソッドを呼び出すことによってそれを行います。

6. インフラストラクチャレイヤー

インフラストラクチャ層には、アプリケーションの実行に必要なロジックが含まれています。

したがって、構成クラスを作成することから始めます。 まず、OrderServiceをSpringbeanとして登録するクラスを実装しましょう。

@Configuration
public class BeanConfiguration {

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

次に、使用するSpring Dataリポジトリを有効にするための構成を作成しましょう。

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

これらのリポジトリはインフラストラクチャ層にのみ存在できるため、basePackageClassesプロパティを使用しました。 したがって、Springがアプリケーション全体をスキャンする理由はありません。 さらに、このクラスには、MongoDBとアプリケーション間の接続の確立に関連するすべてのものを含めることができます。

最後に、ドメインレイヤーからOrderRepositoryを実装します。 実装ではSpringDataMongoOrderRepositoryを使用します。

@Component
public class MongoDbOrderRepository implements OrderRepository {

    private SpringDataMongoOrderRepository orderRepository;

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

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

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

この実装は、OrderをMongoDBに保存します。 六角形のアーキテクチャでは、この実装はアダプタでもあります。

7. 利点

このアプローチの最初の利点は、レイヤーごとに作業を分離することです。 他のレイヤーに影響を与えることなく、1つのレイヤーに集中できます。

さらに、それぞれがそのロジックに焦点を合わせているため、当然理解しやすくなります。

もう1つの大きな利点は、ドメインロジックを他のすべてから分離したことです。 ドメイン部分にはビジネスロジックのみが含まれており、別の環境に簡単に移動できます

実際、Cassandraをデータベースとして使用するようにインフラストラクチャレイヤーを変更してみましょう。

@Component
public class CassandraDbOrderRepository implements OrderRepository {

    private final SpringDataCassandraOrderRepository orderRepository;

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

    @Override
    public Optional<Order> findById(UUID id) {
        Optional<OrderEntity> 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とは異なり、データベース内のドメインを永続化するためにOrderEntityを使用するようになりました。

Orderドメインオブジェクトにテクノロジー固有のアノテーションを追加すると、インフラストラクチャとドメインレイヤー間の分離に違反します

リポジトリは、ドメインを永続性のニーズに適合させます。

さらに一歩進んで、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("<<Create complete order>>");
        UUID orderId = createOrder();
        orderService.completeOrder(orderId);
    }

    public void createIncompleteOrder() {
        LOG.info("<<Create incomplete order>>");
        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. 結論

この記事では、アプリケーションに関連するロジックを特定のレイヤーに分離する方法を学びました。

まず、アプリケーション、ドメイン、インフラストラクチャの3つの主要なレイヤーを定義しました。 その後、それらを埋める方法と利点を説明しました。

次に、各レイヤーの実装を考え出しました。

最後に、ドメインに影響を与えることなく、アプリケーションとインフラストラクチャのレイヤーを交換しました。

いつものように、これらの例のコードはGitHubから入手できます。