1. 概要

ドメイン駆動設計(DDD)は、より高いビジネス価値を提供するための効果的なソフトウェアアーキテクチャの設計に役立つ一連の原則とツールです。 バウンドコンテキストは、アプリケーションドメイン全体を複数の意味的に一貫した部分に分離することにより、大きな泥だんごからアーキテクチャを救済するための中心的かつ不可欠なパターンの1つです。

同時に、 Java 9 Module System を使用すると、強力にカプセル化されたモジュールを作成できます。

このチュートリアルでは、簡単なストアアプリケーションを作成し、Java9モジュールを活用して境界付きコンテキストの明示的な境界を定義する方法を確認します。

2. DDDバウンドコンテキスト

現在、ソフトウェアシステムは単純なCRUDアプリケーションではありません。 実際、典型的なモノリシックエンタープライズシステムは、いくつかのレガシーコードベースと新しく追加された機能で構成されています。 ただし、変更を加えるたびに、このようなシステムを維持することはますます困難になります。 最終的には、完全に保守できなくなる可能性があります。

2.1. 境界のある文脈とユビキタス言語

対処された問題を解決するために、DDDは境界コンテキストの概念を提供します。境界コンテキストは、特定の用語とルールが一貫して適用されるドメインの論理境界です。 この境界の内側では、すべての用語、定義、および概念がユビキタス言語を形成します。

特に、ユビキタス言語の主な利点は、特定のビジネスドメインのさまざまな領域のプロジェクトメンバーをグループ化することです。

さらに、複数のコンテキストが同じもので機能する場合があります。 ただし、これらの各コンテキスト内では異なる意味を持つ場合があります。

2.2. 注文コンテキスト

Order Contextを定義して、アプリケーションの実装を始めましょう。 このコンテキストには、OrderItemCustomerOrderの2つのエンティティが含まれています。

CustomerOrderエンティティは集約ルートです。

public class CustomerOrder {
    private int orderId;
    private String paymentMethod;
    private String address;
    private List<OrderItem> orderItems;

    public float calculateTotalPrice() {
        return orderItems.stream().map(OrderItem::getTotalPrice)
          .reduce(0F, Float::sum);
    }
}

ご覧のとおり、このクラスにはcalculateTotalPriceビジネスメソッドが含まれています。 しかし、実際のプロジェクトでは、おそらくはるかに複雑になります。たとえば、最終価格に割引や税金を含めるなどです。

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

public class OrderItem {
    private int productId;
    private int quantity;
    private float unitPrice;
    private float unitWeight;
}

エンティティを定義しましたが、アプリケーションの他の部分にAPIを公開する必要もあります。 CustomerOrderServiceクラスを作成しましょう。

public class CustomerOrderService implements OrderService {
    public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent";

    private CustomerOrderRepository orderRepository;
    private EventBus eventBus;

    @Override
    public void placeOrder(CustomerOrder order) {
        this.orderRepository.saveCustomerOrder(order);
        Map<String, String> payload = new HashMap<>();
        payload.put("order_id", String.valueOf(order.getOrderId()));
        ApplicationEvent event = new ApplicationEvent(payload) {
            @Override
            public String getType() {
                return EVENT_ORDER_READY_FOR_SHIPMENT;
            }
        };
        this.eventBus.publish(event);
    }
}

ここで、強調すべきいくつかの重要なポイントがあります。 placeOrder メソッドは、顧客の注文を処理します。 注文が処理された後、イベントはEventBusに公開されます。 次の章では、イベント駆動型の通信について説明します。 このサービスは、OrderServiceインターフェースのデフォルトの実装を提供します。

public interface OrderService extends ApplicationService {
    void placeOrder(CustomerOrder order);

    void setOrderRepository(CustomerOrderRepository orderRepository);
}

さらに、このサービスでは、注文を永続化するためにCustomerOrderRepositoryが必要です。

public interface CustomerOrderRepository {
    void saveCustomerOrder(CustomerOrder order);
}

重要なのは、このインターフェイスはこのコンテキスト内に実装されていないが、後で説明するようにインフラストラクチャモジュールによって提供されることです。

2.3. 配送コンテキスト

それでは、配送コンテキストを定義しましょう。 また、単純で、 Parcel PackageItem 、およびShippableOrderの3つのエンティティが含まれます。

ShippableOrderエンティティから始めましょう。

public class ShippableOrder {
    private int orderId;
    private String address;
    private List<PackageItem> packageItems;
}

この場合、エンティティにはpaymentMethodフィールドが含まれていません。 これは、配送コンテキストでは、どの支払い方法が使用されているかを気にしないためです。 Shipping Contextは、注文の出荷を処理するだけの責任があります。

また、ParcelエンティティはShippingContextに固有です。

public class Parcel {
    private int orderId;
    private String address;
    private String trackingId;
    private List<PackageItem> packageItems;

    public float calculateTotalWeight() {
        return packageItems.stream().map(PackageItem::getWeight)
          .reduce(0F, Float::sum);
    }

    public boolean isTaxable() {
        return calculateEstimatedValue() > 100;
    }

    public float calculateEstimatedValue() {
        return packageItems.stream().map(PackageItem::getWeight)
          .reduce(0F, Float::sum);
    }
}

ご覧のとおり、特定のビジネスメソッドも含まれており、集約ルートとして機能します。

最後に、ParcelShippingServiceを定義しましょう。

public class ParcelShippingService implements ShippingService {
    public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent";
    private ShippingOrderRepository orderRepository;
    private EventBus eventBus;
    private Map<Integer, Parcel> shippedParcels = new HashMap<>();

    @Override
    public void shipOrder(int orderId) {
        Optional<ShippableOrder> order = this.orderRepository.findShippableOrder(orderId);
        order.ifPresent(completedOrder -> {
            Parcel parcel = new Parcel(completedOrder.getOrderId(), completedOrder.getAddress(), 
              completedOrder.getPackageItems());
            if (parcel.isTaxable()) {
                // Calculate additional taxes
            }
            // Ship parcel
            this.shippedParcels.put(completedOrder.getOrderId(), parcel);
        });
    }

    @Override
    public void listenToOrderEvents() {
        this.eventBus.subscribe(EVENT_ORDER_READY_FOR_SHIPMENT, new EventSubscriber() {
            @Override
            public <E extends ApplicationEvent> void onEvent(E event) {
                shipOrder(Integer.parseInt(event.getPayloadValue("order_id")));
            }
        });
    }

    @Override
    public Optional<Parcel> getParcelByOrderId(int orderId) {
        return Optional.ofNullable(this.shippedParcels.get(orderId));
    }
}

このサービスも同様に、ShippingOrderRepositoryを使用してIDで注文を取得します。 さらに重要なことに、別のコンテキストによって公開されるOrderReadyForShipmentEventイベントをサブスクライブします。このイベントが発生すると、サービスはいくつかのルールを適用して注文を送信します。 わかりやすくするために、出荷された注文はHashMapに保存されます。

3. コンテキストマップ

これまで、2つのコンテキストを定義しました。 ただし、それらの間に明示的な関係は設定していません。 この目的のために、DDDにはコンテキストマッピングの概念があります。 コンテキストマップは、システムのさまざまなコンテキスト間の関係を視覚的に説明したものです。 このマップは、さまざまなパーツがどのように共存してドメインを形成するかを示しています。

境界コンテキスト間の関係には、主に5つのタイプがあります。

  • パートナーシップ–2つのチームを依存する目標に合わせるために協力する2つのコンテキスト間の関係
  • 共有カーネル–コードの重複を減らすために、いくつかのコンテキストの共通部分が別のコンテキスト/モジュールに抽出されるときの一種の関係
  • Customer-supplier – 2つのコンテキスト間の接続。一方のコンテキスト(アップストリーム)がデータを生成し、もう一方(ダウンストリーム)がデータを消費します。 この関係では、双方が可能な限り最高のコミュニケーションを確立することに関心があります
  • Conformist –この関係にはアップストリームとダウンストリームもありますが、ダウンストリームは常にアップストリームのAPIに準拠しています
  • Anticorruption Layer –このタイプの関係は、レガシーシステムを新しいアーキテクチャに適応させ、レガシーコードベースから徐々に移行するために広く使用されています。 腐敗防止レイヤーは、アダプターとして機能し、アップストリームからのデータを変換し、不要な変更から保護します

特定の例では、共有カーネル関係を使用します。 純粋な形で定義することはしませんが、ほとんどの場合、システム内のイベントのメディエーターとして機能します。

したがって、SharedKernelモジュールには具体的な実装は含まれず、インターフェースのみが含まれます。

EventBusインターフェースから始めましょう。

public interface EventBus {
    <E extends ApplicationEvent> void publish(E event);

    <E extends ApplicationEvent> void subscribe(String eventType, EventSubscriber subscriber);

    <E extends ApplicationEvent> void unsubscribe(String eventType, EventSubscriber subscriber);
}

このインターフェースは、インフラストラクチャモジュールの後半で実装されます。

次に、イベント駆動型通信をサポートするデフォルトのメソッドを使用して、基本サービスインターフェイスを作成します。

public interface ApplicationService {

    default <E extends ApplicationEvent> void publishEvent(E event) {
        EventBus eventBus = getEventBus();
        if (eventBus != null) {
            eventBus.publish(event);
        }
    }

    default <E extends ApplicationEvent> void subscribe(String eventType, EventSubscriber subscriber) {
        EventBus eventBus = getEventBus();
        if (eventBus != null) {
            eventBus.subscribe(eventType, subscriber);
        }
    }

    default <E extends ApplicationEvent> void unsubscribe(String eventType, EventSubscriber subscriber) {
        EventBus eventBus = getEventBus();
        if (eventBus != null) {
            eventBus.unsubscribe(eventType, subscriber);
        }
    }

    EventBus getEventBus();

    void setEventBus(EventBus eventBus);
}

したがって、制限付きコンテキストのサービスインターフェイスは、このインターフェイスを拡張して、共通のイベント関連機能を備えています。

4. Java9のモジュール性

次に、Java9モジュールシステムが定義されたアプリケーション構造をどのようにサポートできるかを探ります。

Javaプラットフォームモジュールシステム(JPMS)は、より信頼性が高く、強力にカプセル化されたモジュールの構築を推奨します。結果として、これらの機能は、コンテキストを分離し、明確な境界を確立するのに役立ちます。

最終的なモジュール図を見てみましょう。

4.1. SharedKernelモジュール

他のモジュールに依存しないSharedKernelモジュールから始めましょう。 したがって、module-info.javaは次のようになります。

module com.baeldung.dddmodules.sharedkernel {
    exports com.baeldung.dddmodules.sharedkernel.events;
    exports com.baeldung.dddmodules.sharedkernel.service;
}

モジュールインターフェイスをエクスポートするため、他のモジュールで使用できます。

4.2. OrderContextモジュール

次に、焦点をOrderContextモジュールに移しましょう。 SharedKernelモジュールで定義されたインターフェースのみが必要です。

module com.baeldung.dddmodules.ordercontext {
    requires com.baeldung.dddmodules.sharedkernel;
    exports com.baeldung.dddmodules.ordercontext.service;
    exports com.baeldung.dddmodules.ordercontext.model;
    exports com.baeldung.dddmodules.ordercontext.repository;
    provides com.baeldung.dddmodules.ordercontext.service.OrderService
      with com.baeldung.dddmodules.ordercontext.service.CustomerOrderService;
}

また、このモジュールがOrderServiceインターフェースのデフォルトの実装をエクスポートしていることがわかります。

4.3. ShippingContextモジュール

前のモジュールと同様に、ShippingContextモジュール定義ファイルを作成しましょう。

module com.baeldung.dddmodules.shippingcontext {
    requires com.baeldung.dddmodules.sharedkernel;
    exports com.baeldung.dddmodules.shippingcontext.service;
    exports com.baeldung.dddmodules.shippingcontext.model;
    exports com.baeldung.dddmodules.shippingcontext.repository;
    provides com.baeldung.dddmodules.shippingcontext.service.ShippingService
      with com.baeldung.dddmodules.shippingcontext.service.ParcelShippingService;
}

同様に、ShippingServiceインターフェイスのデフォルトの実装をエクスポートします。

4.4. インフラストラクチャモジュール

次に、インフラストラクチャモジュールについて説明します。 このモジュールには、定義されたインターフェースの実装の詳細が含まれています。 EventBusインターフェイスの簡単な実装を作成することから始めます。

public class SimpleEventBus implements EventBus {
    private final Map<String, Set<EventSubscriber>> subscribers = new ConcurrentHashMap<>();

    @Override
    public <E extends ApplicationEvent> void publish(E event) {
        if (subscribers.containsKey(event.getType())) {
            subscribers.get(event.getType())
              .forEach(subscriber -> subscriber.onEvent(event));
        }
    }

    @Override
    public <E extends ApplicationEvent> void subscribe(String eventType, EventSubscriber subscriber) {
        Set<EventSubscriber> eventSubscribers = subscribers.get(eventType);
        if (eventSubscribers == null) {
            eventSubscribers = new CopyOnWriteArraySet<>();
            subscribers.put(eventType, eventSubscribers);
        }
        eventSubscribers.add(subscriber);
    }

    @Override
    public <E extends ApplicationEvent> void unsubscribe(String eventType, EventSubscriber subscriber) {
        if (subscribers.containsKey(eventType)) {
            subscribers.get(eventType).remove(subscriber);
        }
    }
}

次に、CustomerOrderRepositoryおよびShippingOrderRepositoryインターフェースを実装する必要があります。 ほとんどの場合、Orderエンティティは同じテーブルに格納されますが、制限されたコンテキストで別のエンティティモデルとして使用されます。

ビジネスドメインのさまざまな領域からの混合コードまたは低レベルのデータベースマッピングを含む単一のエンティティが表示されることは非常に一般的です。 この実装では、境界のあるコンテキストCustomerOrderShippableOrderに従ってエンティティを分割しました。

まず、永続モデル全体を表すクラスを作成しましょう。

public static class PersistenceOrder {
    public int orderId;
    public String paymentMethod;
    public String address;
    public List<OrderItem> orderItems;

    public static class OrderItem {
        public int productId;
        public float unitPrice;
        public float itemWeight;
        public int quantity;
    }
}

このクラスには、CustomerOrderエンティティとShippableOrderエンティティの両方のすべてのフィールドが含まれていることがわかります。

物事を単純にするために、インメモリデータベースをシミュレートしましょう。

public class InMemoryOrderStore implements CustomerOrderRepository, ShippingOrderRepository {
    private Map<Integer, PersistenceOrder> ordersDb = new HashMap<>();

    @Override
    public void saveCustomerOrder(CustomerOrder order) {
        this.ordersDb.put(order.getOrderId(), new PersistenceOrder(order.getOrderId(),
          order.getPaymentMethod(),
          order.getAddress(),
          order
            .getOrderItems()
            .stream()
            .map(orderItem ->
              new PersistenceOrder.OrderItem(orderItem.getProductId(),
                orderItem.getQuantity(),
                orderItem.getUnitWeight(),
                orderItem.getUnitPrice()))
            .collect(Collectors.toList())
        ));
    }

    @Override
    public Optional<ShippableOrder> findShippableOrder(int orderId) {
        if (!this.ordersDb.containsKey(orderId)) return Optional.empty();
        PersistenceOrder orderRecord = this.ordersDb.get(orderId);
        return Optional.of(
          new ShippableOrder(orderRecord.orderId, orderRecord.orderItems
            .stream().map(orderItem -> new PackageItem(orderItem.productId,
              orderItem.itemWeight,
              orderItem.quantity * orderItem.unitPrice)
            ).collect(Collectors.toList())));
    }
}

ここでは、永続モデルを適切なタイプに変換したり、適切なタイプから変換したりすることで、さまざまなタイプのエンティティを永続化および取得します。

最後に、モジュール定義を作成しましょう。

module com.baeldung.dddmodules.infrastructure {
    requires transitive com.baeldung.dddmodules.sharedkernel;
    requires transitive com.baeldung.dddmodules.ordercontext;
    requires transitive com.baeldung.dddmodules.shippingcontext;
    provides com.baeldung.dddmodules.sharedkernel.events.EventBus
      with com.baeldung.dddmodules.infrastructure.events.SimpleEventBus;
    provides com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository
      with com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore;
    provides com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository
      with com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore;
}

Provides with 句を使用して、他のモジュールで定義されたいくつかのインターフェイスの実装を提供しています。

さらに、このモジュールは依存関係のアグリゲーターとして機能するため、requiretransitiveキーワードを使用します。 その結果、インフラストラクチャモジュールを必要とするモジュールは、これらすべての依存関係を一時的に取得します。

4.5. メインモジュール

結論として、アプリケーションへのエントリポイントとなるモジュールを定義しましょう。

module com.baeldung.dddmodules.mainapp {
    uses com.baeldung.dddmodules.sharedkernel.events.EventBus;
    uses com.baeldung.dddmodules.ordercontext.service.OrderService;
    uses com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository;
    uses com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository;
    uses com.baeldung.dddmodules.shippingcontext.service.ShippingService;
    requires transitive com.baeldung.dddmodules.infrastructure;
}

インフラストラクチャモジュールに推移的な依存関係を設定しただけなので、ここで明示的に要求する必要はありません。

一方、これらの依存関係はusedキーワードでリストされています。 used 句は、次の章で説明する ServiceLoader に、このモジュールがこれらのインターフェイスを使用することを指示します。 ただし、コンパイル時に実装が利用可能である必要はありません。

5. アプリケーションの実行

最後に、アプリケーションを構築する準備がほぼ整いました。 プロジェクトの構築にはMavenを活用します。 これにより、モジュールの操作がはるかに簡単になります。

5.1. プロジェクト構造

私たちのプロジェクトには、5つのモジュールと親モジュールが含まれています。 プロジェクトの構造を見てみましょう。

ddd-modules (the root directory)
pom.xml
|-- infrastructure
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- com.baeldung.dddmodules.infrastructure
    pom.xml
|-- mainapp
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- com.baeldung.dddmodules.mainapp
    pom.xml
|-- ordercontext
    |-- src
        |-- main
            | -- java
            module-info.java
            |--com.baeldung.dddmodules.ordercontext
    pom.xml
|-- sharedkernel
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- com.baeldung.dddmodules.sharedkernel
    pom.xml
|-- shippingcontext
    |-- src
        |-- main
            | -- java
            module-info.java
            |-- com.baeldung.dddmodules.shippingcontext
    pom.xml

5.2. 主な用途

これで、メインアプリケーション以外のすべてが揃ったので、mainメソッドを定義しましょう。

public static void main(String args[]) {
    Map<Class<?>, Object> container = createContainer();
    OrderService orderService = (OrderService) container.get(OrderService.class);
    ShippingService shippingService = (ShippingService) container.get(ShippingService.class);
    shippingService.listenToOrderEvents();

    CustomerOrder customerOrder = new CustomerOrder();
    int orderId = 1;
    customerOrder.setOrderId(orderId);
    List<OrderItem> orderItems = new ArrayList<OrderItem>();
    orderItems.add(new OrderItem(1, 2, 3, 1));
    orderItems.add(new OrderItem(2, 1, 1, 1));
    orderItems.add(new OrderItem(3, 4, 11, 21));
    customerOrder.setOrderItems(orderItems);
    customerOrder.setPaymentMethod("PayPal");
    customerOrder.setAddress("Full address here");
    orderService.placeOrder(customerOrder);

    if (orderId == shippingService.getParcelByOrderId(orderId).get().getOrderId()) {
        System.out.println("Order has been processed and shipped successfully");
    }
}

主な方法について簡単に説明しましょう。 この方法では、以前に定義されたサービスを使用して、単純な顧客注文フローをシミュレートしています。 最初に、3つのアイテムで注文を作成し、必要な配送と支払いの情報を提供しました。 次に、注文を送信し、最終的に発送と処理が正常に行われたかどうかを確認しました。

しかし、どのようにしてすべての依存関係を取得し、なぜ createContainer メソッドリターン地図 オブジェクト>? この方法を詳しく見てみましょう。

5.3. ServiceLoaderを使用した依存性注入

このプロジェクトでは、 Spring IoC の依存関係がないため、代わりに ServiceLoaderAPIを使用してサービスの実装を検出します。 これは新しい機能ではありません— ServiceLoaderAPI自体はJava6から存在しています。

ServiceLoaderクラスの静的loadメソッドの1つを呼び出すことにより、ローダーインスタンスを取得できます。 loadメソッドはIterable型を返すため、検出された実装を反復処理できます。

それでは、ローダーを適用して依存関係を解決しましょう。

public static Map<Class<?>, Object> createContainer() {
    EventBus eventBus = ServiceLoader.load(EventBus.class).findFirst().get();

    CustomerOrderRepository customerOrderRepository = ServiceLoader.load(CustomerOrderRepository.class)
      .findFirst().get();
    ShippingOrderRepository shippingOrderRepository = ServiceLoader.load(ShippingOrderRepository.class)
      .findFirst().get();

    ShippingService shippingService = ServiceLoader.load(ShippingService.class).findFirst().get();
    shippingService.setEventBus(eventBus);
    shippingService.setOrderRepository(shippingOrderRepository);
    OrderService orderService = ServiceLoader.load(OrderService.class).findFirst().get();
    orderService.setEventBus(eventBus);
    orderService.setOrderRepository(customerOrderRepository);

    HashMap<Class<?>, Object> container = new HashMap<>();
    container.put(OrderService.class, orderService);
    container.put(ShippingService.class, shippingService);

    return container;
}

ここでは、必要なすべてのインターフェイスに対して静的ロードメソッドを呼び出しています。これにより、毎回新しいローダーインスタンスが作成されます。その結果、解決済みの依存関係はキャッシュされません。代わりに、作成されます。毎回新しいインスタンス。

通常、サービスインスタンスは2つの方法のいずれかで作成できます。 サービス実装クラスには、パブリックの引数なしコンストラクターが必要であるか、静的プロバイダーメソッドを使用する必要があります。

結果として、ほとんどのサービスには、依存関係のための引数なしのコンストラクターとセッターメソッドがあります。 ただし、すでに見てきたように、 InMemoryOrderStore クラスは、CustomerOrderRepositoryShippingOrderRepositoryの2つのインターフェイスを実装しています。

ただし、 load メソッドを使用してこれらの各インターフェイスをリクエストすると、InMemoryOrderStoreのさまざまなインスタンスが取得されます。 これは望ましくない動作なので、プロバイダーメソッドの手法を使用してインスタンスをキャッシュしましょう。

public class InMemoryOrderStore implements CustomerOrderRepository, ShippingOrderRepository {
    private volatile static InMemoryOrderStore instance = new InMemoryOrderStore();

    public static InMemoryOrderStore provider() {
        return instance;
    }
}

シングルトンパターンを適用して、 InMemoryOrderStore クラスの単一インスタンスをキャッシュし、プロバイダーメソッドから返します。

サービスプロバイダーがproviderメソッドを宣言した場合、 ServiceLoader はこのメソッドを呼び出して、サービスのインスタンスを取得します。 それ以外の場合は、Reflectionを介して引数なしのコンストラクターを使用してインスタンスを作成しようとします。 その結果、 createContainer メソッドに影響を与えることなく、サービスプロバイダーのメカニズムを変更できます。

そして最後に、セッターを介して解決された依存関係をサービスに提供し、構成されたサービスを返します。

最後に、アプリケーションを実行できます。

6. 結論

この記事では、いくつかの重要なDDDの概念、つまり、境界付きコンテキスト、ユビキタス言語、およびコンテキストマッピングについて説明しました。 システムを境界コンテキストに分割することには多くの利点がありますが、同時に、このアプローチをどこにでも適用する必要はありません。

次に、Java9モジュールシステムとBoundedContextを使用して、強力にカプセル化されたモジュールを作成する方法を説明しました。

さらに、依存関係を検出するためのデフォルトのServiceLoaderメカニズムについても説明しました。

プロジェクトの完全なソースコードは、GitHubから入手できます。