4年前 (2021-03-25)  Java系列 |   抢沙发  672 
文章评分 0 次,平均分 0.0

领域驱动设计(Domain-Driven Design,DDD)是一套帮助我们设计有效的软件体系结构以提供更高业务价值的原则和工具。有界上下文(Bounded Context)是将整个应用程序域划分为多个语义一致的部分,从而将体系结构从一个大泥球中解救出来的核心和基本模式之一。

同时,利用java9模块系统,我们可以创建强封装的模块。

在本文中,我们将创建一个简单的存储应用程序,并了解如何利用Java9 Module为有界上下文定义显式边界。

DDD Bounded Contexts 限定上下文

如今,软件系统不是简单的CRUD应用程序。实际上,典型的单片企业系统由一些遗留的代码库和新添加的特性组成。然而,随着每一个变化,维护这样的系统变得越来越困难。最终,它可能会变得完全无法维护。

有界上下文与泛在语言

为了解决这个问题,DDD提出了有界上下文的概念。有界上下文是一个域的逻辑边界,其中特定的术语和规则始终适用。在这个边界内,所有的术语、定义和概念构成了无处不在的语言。

特别是,泛在语言的主要好处是将来自特定业务领域不同领域的项目成员组合在一起。

另外,多个上下文可以处理同一件事。然而,在这些语境中,它可能有不同的含义。
领域驱动设计中的限界上下文和Java9的Module关系

Order上下文

让我们从定义Order上下文开始实现我们的应用程序。此上下文包含两个实体:OrderItemCustomerOrder
领域驱动设计中的限界上下文和Java9的Module关系
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);
}

重要的是,这个接口不是在这个上下文中实现的,而是由基础设施模块提供的,我们将在后面看到

Shipping上下文

现在,让我们定义Shipping上下文。它也很简单,包含三个实体:ParcelPackageItemShippableOrder
领域驱动设计中的限界上下文和Java9的Module关系
让我们从ShippableOrder实体开始:

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

在本例中,实体不包含paymentMethod字段。这是因为,在我们的Shipping环境中,我们不在乎使用哪种付款方式。Shipping上下文只负责处理订单的shipments

此外,Parcel实体特定于Shipping上下文:

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

此服务类似地使用ShippingOrderRepositoryid获取订单。更重要的是,它订阅OrderReadyForShipEvent事件,该事件由另一上下文发布。当发生此事件时,服务应用一些规则并发送订单。为了简单起见,我们将装运的订单存储在HashMap中。

上下文映射

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

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

  • 合作伙伴关系——两个环境之间的关系,它们合作使两个团队与依赖的目标保持一致
  • 共享内核——当将多个上下文的公共部分提取到另一个上下文/模块以减少代码重复时的一种关系
  • 客户供应商–两个上下文之间的连接,其中一个上下文(上游)生成数据,另一个(下游)使用数据。在这种关系中,双方都有兴趣建立尽可能好的沟通
  • 符合者–此关系也有上游和下游,但是下游始终符合上游API
  • 反腐败层——这种关系广泛用于遗留系统,以使它们适应新的体系结构,并逐渐从遗留代码库迁移。反腐败层充当适配器,用于从上游转换数据,并防止不希望的更改

在我们的具体示例中,我们将使用共享内核关系。我们不会用它的纯粹形式来定义它,但它将主要充当系统中事件的中介。

因此,Sharedkenl模块不包含任何具体实现,只包含接口。

让我们从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模块化

现在,是时候探索Java9模块系统如何支持已定义的应用程序结构了。

Java平台模块系统(javaplatformmodulesystem,JPMS)鼓励构建更可靠、更强封装的模块。因此,这些特性可以帮助隔离我们的上下文并建立清晰的边界。

让我们看看最后的模块图:
领域驱动设计中的限界上下文和Java9的Module关系

SharedKernel模块

让我们从SharedKernel模块开始,它对其他模块没有任何依赖关系。所以,module-info.java看起来像:

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

我们导出模块接口,以便其他模块可以使用它们。

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接口的默认实现。

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接口的默认实现。

基础设施模块

现在是描述基础设施模块的时候了。此模块包含已定义接口的实现详细信息。我们将首先为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);
        }
    }
}

接下来,我们需要实现CustomerOrderRepositoryShippingOrderRepository接口。在大多数情况下,订单实体将存储在同一个表中,但在有界上下文中用作不同的实体模型。

很常见的情况是,单个实体包含来自业务域不同区域的混合代码或低级数据库映射。对于我们的实现,我们已经根据有界上下文分割了实体: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;
    }
}

我们可以看到这个类包含CustomerOrderShippableOrder实体的所有字段。

为了简单起见,让我们模拟内存中的数据库:

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子句,我们提供了在其他模块中定义的一些接口的实现。

此外,此模块充当依赖项的聚合器,因此我们使用requires-transitive关键字。因此,需要基础结构模块的模块将以可传递的方式获得所有这些依赖关系。

主模块

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

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关键字列出这些依赖项。uses子句指示ServiceLoader该模块希望使用这些接口。但是,它不要求实现在编译时可用。

运行应用程序

最后,我们几乎准备好构建应用程序了。我们将利用Maven来构建我们的项目。这使得使用模块更加容易。

项目结构

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

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

主应用

到目前为止,除了主应用程序之外,我们已经拥有了所有内容,因此让我们定义我们的主方法:

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>? 让我们仔细看看这个方法。

使用ServiceLoader的依赖注入

在这个项目中,我们没有任何Spring IoC依赖项,因此,我们也可以使用ServiceLoader API来发现服务的实现。这不是一个新特性—ServiceLoaderAPI本身从Java6开始就存在了。

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

这里,我们为每个需要的接口调用静态加载方法,每次都会创建一个新的加载程序实例。因此,它不会缓存已解析的依赖项,而是每次都会创建新实例。

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

因此,我们的大多数服务都没有用于依赖关系的arg构造函数和setter方法。但是,正如我们已经看到的,InMemoryOrderStore类实现了两个接口:CustomerOrderRepositoryShippingOrderRepository

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

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

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

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

如果服务提供程序声明了一个提供程序方法,那么ServiceLoader将调用此方法来获取服务实例。否则,它将尝试通过反射使用无参数构造函数创建实例。因此,我们可以在不影响createContainer方法的情况下更改服务提供者机制。

最后,我们通过setter向服务提供已解析的依赖关系,并返回已配置的服务。

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

结论

在本文中,我们讨论了一些关键的DDD概念:有界上下文、泛在语言和上下文映射。虽然将系统划分为有限的上下文有很多好处,但同时,没有必要在任何地方都应用这种方法。

接下来,我们将看到如何使用Java9模块系统和有界上下文来创建强封装的模块。

此外,我们还介绍了用于发现依赖项的默认ServiceLoader机制。

项目的完整源代码:https://github.com/eugenp/tutorials/tree/master/ddd-modules

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1758.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册