用Java9模块实现DDD有界上下文 | Baeldung

20-03-20 banq

领域驱动设计(DDD)是一组原则和工具,可帮助我们设计有效的软件体系结构以提供更高的业务价值。通过将整个应用程序域分离为多个语义一致的部分,Bounded Context是从架构的泥潭中拯救体系结构的主要模式之一。同时,借助Java 9 Module System,我们可以创建高度封装的模块。

在本教程中,我们将创建一个简单的商店应用程序,并了解如何在定义有限上下文的显式边界时利用Java 9模块。(一个有界上下文对应一个Java9模块)

DDD有界上下文

如今,软件系统已不是简单的CRUD应用程序。实际上,典型的单体式企业系统由一些旧代码库和新添加的功能组成。但是,在进行每次更改后维护此类系统变得越来越困难。最终,它可能变得完全无法维护。

为了解决问题,DDD提供了“边界上下文”的概念。有界上下文是特定条款和规则一致适用的域的逻辑边界。在此边界内,所有术语,定义和概念都构成了通用语言。

特别是,统一通用语言的主要好处是将来自特定业务领域不同领域的项目成员团聚在一起。此外,多个上下文可能对同一事物起作用。但是,在每个上下文中它可能具有不同的含义。

订单上下文

让我们通过定义Order Context开始实现我们的应用程序。该上下文包含两个实体:OrderItem和CustomerOrder。(banq注:DDD关键是如何得到Order Context,这里没有讨论,也不是重点,主要讨论实现)

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

至关重要的是,该接口不是在此上下文中实现的,而是由基础架构模块提供的,我们将在后面看到。

运输上下文

运输上下文包含三个实体:Parcel,PackageItem和ShippableOrder。

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

在这种情况下,实体不包含paymentMethod字段。这是因为,在我们的运输上下文中,我们不在乎使用哪种付款方式。运输上下文仅负责处理订单装运。

特定于运输上下文的包裹Parcel实体:

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中

两个上下文映射

到目前为止,我们定义了两个上下文。但是,我们没有在它们之间设置任何明确的关系。为此,DDD具有上下文映射的概念。上下文映射是系统不同上下文之间关系的直观描述。该图显示了不同部分如何共存在一起以形成域。

有界上下文之间的关系主要有五种类型:

  • 伙伴关系 –两种环境之间的关系,可以相互协作以使两个团队具有相互依赖的目标
  • 共享内核 –一种将多个上下文的公共部分提取到另一个上下文/模块以减少代码重复的一种关系
  • 客户-供应商 –两个上下文之间的连接,其中一个上下文(上游)产生数据,而另一个上下文(下游)消耗数据。在这种关系中,双方都希望建立尽可能最好的沟通
  • 遵从者 -这种关系也有上游和下游,但是下游始终符合上游的API
  • 反腐败层 –这种类型的关系广泛用于遗留系统,以使其适应新的体系结构并逐渐从遗留代码库迁移。反腐败层充当适配器,以转换来自上游的数据并保护免受意外更改

在我们的特定示例中,我们将使用共享内核关系。我们不会以其纯粹的形式对其进行定义,但是它将主要充当系统中事件的中介者。(banq注:共享内核时最坏的方式,会形成单体的对共享内核的高度依赖)

因此,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);
}

有界上下文中的服务接口扩展了该接口,使其具有与事件相关的通用功能。

Java 9模块化

Java平台模块系统(JPMS)鼓励构建更可靠和高度封装的模块。结果,这些功能可以帮助隔离我们的上下文并建立清晰的边界。

让我们从共享内核SharedKernel模块开始,该模块对其他模块没有任何依赖性。因此,module-info.java看起来像:

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

我们导出模块接口,让它可用于其他模块。

接下来,让我们将焦点移到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接口的默认实现。

创建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  接口导出了默认实现。

现在是时候描述基础架构模块了。该模块包含已定义接口的实现详细信息。我们将从为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实体将存储在同一表中,但在有界上下文中用作不同的实体模型。

常见的情况是,一个实体包含来自业务领域不同领域或低级数据库映射的混合代码。对于我们的实现,我们根据有界的上下文来拆分我们的实体:CustomerOrder和ShippableOrder。

首先,让我们创建一个代表整个持久模型的类:

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

这里提供了在其他模块中定义的几个接口的实现。

此外,此模块充当依赖项的聚集器,因此我们使用require传递关键字。结果,需要基础结构模块的模块将可传递地获得所有这些依赖关系。

最后,让我们定义一个模块,它将成为我们应用程序的入口点:

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

由于我们仅在基础结构模块上设置了传递依赖项,因此在这里不需要明确要求它们。

另一方面,我们使用uses关键字列出这些依赖关系。该模块要使用这些接口。

运行

我们的项目包含五个模块和父模块。让我们看一下我们的项目结构:

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

到目前为止,除了主应用程序外,我们已经拥有一切,因此让我们定义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");
    }
}

在这个方法中,我们通过使用先前定义的服务来模拟简单的客户订单流程。首先,我们创建了包含三个项目的订单,并提供了必要的运输和付款信息。接下来,我们提交了订单,最后检查了订单是否已成功发货和处理。

但是我们如何获得所有依赖关系,为什么createContainer方法返回Map <Class <?>, Object>?让我们仔细看看这种方法。

在此项目中,我们没有任何Spring IoC依赖项,因此,我们将使用ServiceLoader API来发现服务的实现。这不是一个新功能- 自Java 6起就出现了ServiceLoader API本身。

我们可以通过调用ServiceLoader类的静态加载方法之一来获得一个加载器实例。该负载方法返回的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;
}

在这里,我们为所需的每个接口调用静态加载方法,每次都会创建一个新的加载器实例。结果,它不会缓存已经解决的依赖关系,而是每次都会创建新的实例。

通常,可以通过以下两种方式之一创建服务实例。服务实现类必须具有公共的无参数构造函数,或者必须使用静态提供程序方法。

因此,我们的大多数服务都具有无参数构造函数和用于依赖项的setter方法。但是,正如我们已经看到的那样,InMemoryOrderStore类实现了两个接口:CustomerOrderRepository和ShippingOrderRepository。

但是,如果我们使用load方法请求这些接口中的每个接口,我们将获得InMemoryOrderStore的不同实例。这不是理想的行为,因此让我们使用提供者方法技术来缓存实例:

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

我们已经应用了Singleton模式来缓存InMemoryOrderStore类的单个实例,并从provider方法返回它。

如果服务提供者声明了提供者方法,则ServiceLoader调用此方法以获得服务的实例。否则,它将尝试通过Reflection使用无参数构造函数创建实例。结果,我们可以更改服务提供者机制,而不会影响我们的createContainer方法。

现在,我们通过设置器提供对服务的已解决依赖关系,并返回已配置的服务。

最后,我们总算可以运行该应用程序。

结论

在本文中,我们讨论了一些关键的DDD概念:绑定上下文,泛在语言和上下文映射。虽然将系统划分为“有界上下文”有很多好处,但与此同时,无需在所有地方都采用这种方法。接下来,我们已经看到了如何使用Java 9 Module System和Bounded Context来创建高度封装的模块。此外,我们介绍了用于发现依赖项的默认ServiceLoader机制。

该项目的完整源代码可在GitHub上获得

 

                   

1
猜你喜欢