# ddd-ecommerce-backend **Repository Path**: actual-combat-study/ddd-ecommerce-backend ## Basic Information - **Project Name**: ddd-ecommerce-backend - **Description**: Extract ecommerce-backend from master and put it to ecommerce-backend branch - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-05-21 - **Last Updated**: 2021-05-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 1.概述 我们将使用DDD实现Spring应用程序。 此外,我们将在六边形架构的帮助下组织层。 使用这种方法,我们可以轻松地交换应用程序的不同层。 # 2.六边形架构 六边形体系结构是**围绕领域逻辑设计软件应用程序**以将其与外部因素隔离的模型。 领域逻辑是在业务核心中指定的,我们将其称为内部部分,其余部分为外部部分。 通过端口和适配器可以从外部访问领域逻辑。 ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a0948bbae8646009f7297e477d90ff2~tplv-k3u1fbpfcp-watermark.image) # 3.原则 首先,我们应该定义原则来划分我们的代码。 正如已经简要解释过的,六边形结构定义了**内部和外部**。 相反,我们要做的是将应用程序分为三层: **应用程序(外部),域(内部)和基础结构(外部)**: ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e5923455eaf8407f95ca5cec2030b140~tplv-k3u1fbpfcp-watermark.image) 通过**应用程序层,用户或任何其他程序与应用程序**进行交互。 该区域应包含用户界面,RESTful控制器和JSON序列化库之类的内容。 它包括**任何可以向我们的应用程序公开和协调域逻辑执行的内容**。 **在领域层中,我们保留接触并实现业务逻辑的代码**。 这是我们应用程序的核心。 此外,该层应与应用程序部分和基础结构部分都隔离。 最重要的是,它还应该包含定义API以便与域交互的外部部分(例如数据库)进行通信的接口。 最后,**基础架构层是包含应用程序需要运行的所有内容的部分**,例如数据库配置或Spring配置。 此外,它还从域层实现与基础结构相关的接口。 整体目录结构:
├─main
│ ├─java
│ │ └─com
│ │-----└─ecommerce
│ │---------└─onion
│ │--------------├─application
│ │--------------│ ├─cli
│ │--------------│ ├─request
│ │--------------│ └─response
│ │--------------├─domain
│ │--------------│ ├─repository
│ │--------------│ └─service
│ │--------------└─infrastracture
│ │------------------├─configuration
│ │------------------└─repository
│ │----------------------└─mongo
│ └─resources
└─test
├─java
│--└─com
│------└─ecommerce
│----------└─onion
│--------------└─domain
│------------------└─service
└─resources # 4. Domain Layer 让我们从实现我们的核心层(即领域层)开始。 首先,我们应该创建Order类: ```java package com.ecommerce.onion.domain; import java.math.BigDecimal; import java.util.*; /** * @packageName: domain(领域包) * @className: Order(订单领域) * @description: 围绕此订单领域展开业务 * @author: luds * @version: v1.0 * @date: 2021-03/25 */ public class Order { /** * 订单ID */ private UUID id; /** * 订单状态(已创建,已完成) */ private OrderStatus status; /** * 订单项目列表 */ private List orderItems; /** * 订单价格 */ private BigDecimal price; /** * 默认构造器 */ public Order(){} /** * 通过订单ID及产品构建订单 * @param id: 订单ID * @param product: 产品领域实例 */ public Order(final UUID id, final Product product) { this.id = id; //由产品实例返回一个不变的可序列化的订单项目列表 this.orderItems = new ArrayList<>(Collections.singletonList(new OrderItem(product))); //初始状态为创建 this.status = OrderStatus.CREATED; //采用产品价格作为订单价格 this.price = product.getPrice(); } /** * 完成订单 */ public void complete() { //验证状态 validateState(); //把订单状态更新成:已完成 this.status = OrderStatus.COMPLETED; } /** * 由产品追加新订单 * @param product: 产品实例 */ public void addOrder(final Product product) { //验证状态 validateState(); //验证产品 validateProduct(product); //把产品制作的订单追加到订单项目列表 orderItems.add(new OrderItem(product)); //追加新订单后,在订单价格基础上+产品价格 price = price.add(product.getPrice()); } /** * 根据ID来移除订单 * @param id: 订单ID */ public void removeOrder(final UUID id) { //验证状态 validateState(); //根据订单ID,获取订单项目 final OrderItem orderItem = getOrderItem(id); //从订单项目列表移除此订单项目 orderItems.remove(orderItem); //订单价格也扣除此订单项目的价格 price = price.subtract(orderItem.getPrice()); } /** * 根据订单ID获取订单项目 * @param id 订单ID * @return 订单项目 */ private OrderItem getOrderItem(final UUID id) { //如果订单项目的产品ID过滤后,返回第一个订单项目;否则抛出异常:此ID的产品不存在。 return orderItems.stream() .filter(orderItem -> orderItem.getProductId() .equals(id)) .findFirst() .orElseThrow(() -> new DomainException("Product with " + id + " doesn't exist.")); } /** * 验证状态 */ private void validateState() { //如果订单已经完成,就会抛出异常:这个订单已经完成 if (OrderStatus.COMPLETED.equals(status)) { throw new DomainException("The order is in completed state."); } } /** * 验证产品 * @param product: 产品实例 */ private void validateProduct(final Product product) { // 如果产品是空的,就会抛出异常:这个产品不能是空的 if (product == null) { throw new DomainException("The product cannot be null."); } } /** * 获取订单ID * @return 返回订单ID */ public UUID getId() { return id; } /** * 获取订单状态 * @return 返回订单状态 */ public OrderStatus getStatus() { return status; } /** * 获取订单价格 * @return 返回价格 */ public BigDecimal getPrice() { return price; } /** * 获取订单项目列表 * @return 返回订单项目列表 */ public List getOrderItems() { //变不了的列表 return Collections.unmodifiableList(orderItems); } /** * 由ID、订单项目、价格、订单状态,组合成哈希值 * @return 返回哈希值 */ @Override public int hashCode() { return Objects.hash(id, orderItems, price, status); } /** * 如果是此订单本身就返回true,如果不是订单就返回false; * 除了上述两种情况:如果订单ID、订单项目、价格、状态都相同,就返回true,否则false * @param obj: 比较对象 * @return 比较结果 */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Order)) return false; Order other = (Order) obj; return Objects.equals(id, other.id) && Objects.equals(orderItems, other.orderItems) && Objects.equals(price, other.price) && status == other.status; } } ``` **这是我们的聚合根**。 与我们的业务逻辑有关的所有内容都将实践一下。 另外,Order负责将自己保持在正确的状态: - 只能使用给定的ID并基于一个产品创建订单-构造函数本身也以CREATED状态创建订单 - 订单完成后,便无法更改OrderItems - 像设置器一样,不可能从领域对象外部更改Order。 此外,Order类还负责创建其OrderItem。 然后创建OrderItem类: ```java package com.ecommerce.onion.domain; import java.math.BigDecimal; import java.util.Objects; import java.util.UUID; /** * @packageName: domain(领域包) * @className: OrderItem(订单项目领域) * @description: 订单项目列表中使用的订单项目 * @author: luds * @version: v1.0 * @date: 2021-03/25 */ public class OrderItem { /** * 订单项目ID */ private UUID productId; /** * 订单项目价格 */ private BigDecimal price; /** * 默认构造器 */ public OrderItem(){} /** * 由产品构建订单项目 * @param product 产品实例 */ public OrderItem(final Product product) { // 产品ID取出来,给订单项目ID this.productId = product.getId(); // 产品价格取出来,给订单项目价格 this.price = product.getPrice(); } /** * 获取订单项目ID * @return 返回订单项目ID */ public UUID getProductId() { return productId; } /** * 获取订单项目价格 * @return 返回订单项目u价格 */ public BigDecimal getPrice() { return price; } /** * 如果是此订单项目本身就返回true,如果不是订单项目就返回false; * 除了上述两种情况:如果订单项目ID、价格都相同,就返回true,否则false * @param o: 比较对象 * @return 比较结果 */ @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; OrderItem orderItem = (OrderItem) o; return Objects.equals(productId, orderItem.productId) && Objects.equals(price, orderItem.price); } /** * 由ID、订单项目价格组合成哈希值 * @return 返回哈希值 */ @Override public int hashCode() { return Objects.hash(productId, price); } } ``` 如我们所见,OrderItem是基于产品创建的。 它保留对它的引用并存储产品的当前价格。 <订单领域结构图(PlantUML)> ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/39d77f8894b14e9682a56c4ae08fcb46~tplv-k3u1fbpfcp-watermark.image) 接下来,我们将创建一个存储库接口(Hexagonal Architecture中的端口)。 该接口的实现将在基础设施层中: ```java package com.ecommerce.onion.domain.repository; import com.ecommerce.onion.domain.Order; import java.util.Optional; import java.util.UUID; /** * @packageName: domain(领域.资料库) * @className: Order(订单资料库) * @description: 围绕订单资料库的接口定义 * @author: luds * @version: v1.0 * @date: 2021-03/25 */ public interface OrderRepository { /** * 由ID寻找订单 * @param id 订单ID * @return 返回订单 */ Optional findById(UUID id); /** * 持久化订单 * @param order 订单实例 */ void save(Order order); } ``` 最后,我们应确保在每次操作后始终保存订单。 为此,**我们将定义一个领域服务,该领域服务通常包含不能成为根目录一部分的逻辑**: ```java package com.ecommerce.onion.domain.service; import com.ecommerce.onion.domain.Order; import com.ecommerce.onion.domain.Product; import com.ecommerce.onion.domain.repository.OrderRepository; import java.util.UUID; /** * @packageName: domain(领域.服务包) * @className: DomainOrderService(领域订单服务) * @description: 实现订单服务 * @author: luds * @version: v1.0 * @date: 2021-03/25 */ public class DomainOrderService implements OrderService { /** * 订单资料库 */ private final OrderRepository orderRepository; /** * 由订单资料库构建此领域订单服务 * @param orderRepository 订单资料库 */ public DomainOrderService(final OrderRepository orderRepository) { this.orderRepository = orderRepository; } /** * 由此产品创建订单,并返回订单ID * @param product 产品实例 * @return 返回订单ID */ @Override public UUID createOrder(final Product product) { //由UUID.randomUUID()、产品创建订单 final Order order = new Order(UUID.randomUUID(), product); //调用资料库把创建的订单进行持久化 orderRepository.save(order); //从订单返回ID return order.getId(); } /** * 往此订单ID上追加产品 * @param id 订单ID * @param product 产品实例 */ @Override public void addProduct(final UUID id, final Product product) { //由ID获取订单 final Order order = getOrder(id); //往订单上追加产品 order.addOrder(product); //调用资料库把加好的订单,并进行持久化 orderRepository.save(order); } /** * 完成此ID的订单 * @param id 订单ID */ @Override public void completeOrder(final UUID id) { //由ID获取订单 final Order order = getOrder(id); //并完成此订单 order.complete(); //调用资料库把完成的订单,进行持久化 orderRepository.save(order); } /** * 从订单里删除此ID的产品 * @param id 订单ID * @param productId 产品ID */ @Override public void deleteProduct(final UUID id, final UUID productId) { //由ID获取订单 final Order order = getOrder(id); //并从订单上移除为ID的产品 order.removeOrder(productId); //调用资料库把移除的订单,进行持久化 orderRepository.save(order); } /** * 由此ID获取订单 * @param id 订单ID * @return 返回订单 */ private Order getOrder(UUID id) { //调用资料库,由ID查找;如果找到返回订单,否则抛出异常:此ID的订单不存在 return orderRepository .findById(id) .orElseThrow(() -> new RuntimeException("Order with given id doesn't exist")); } } ``` <领域订单服务的结构图(PlantUML)> ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7535eeeb41df4221bde7ab87a7934bed~tplv-k3u1fbpfcp-watermark.image) <领域订单服务的调用关系图(PlantUML)> - createOrder ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/48495cb949f144fb8b4348049aecc8b2~tplv-k3u1fbpfcp-watermark.image) - addProduct ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/560bb9a4ca78434d91af03a80b1c5d16~tplv-k3u1fbpfcp-watermark.image) - completeOrder ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bd995040b34540eebc9156752bc1751b~tplv-k3u1fbpfcp-watermark.image) - deleteProduct ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc843f0c211245f0a4cac0eca0978c3e~tplv-k3u1fbpfcp-watermark.image) 在六边形体系结构中,此服务是实现端口的适配器。 另外,我们不会将其注册为Spring Bean,因为从领域的角度来看,这在内部,而Spring配置在外部。 稍后,我们将在基础架构层中将其与Spring手动连接。 因为领域层与应用程序层和基础结构层完全分离,所以我们也可以独立地对其进行测试: ```java package com.ecommerce.onion.domain.service; import com.ecommerce.onion.domain.Order; import com.ecommerce.onion.domain.OrderProvider; import com.ecommerce.onion.domain.Product; import com.ecommerce.onion.domain.repository.OrderRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import java.math.BigDecimal; import java.util.Optional; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; import static org.mockito.internal.verification.VerificationModeFactory.times; /** * @packageName: domain(领域.服务包) * @className: DomainOrderServiceUnitTest(领域订单服务单体测试) * @description: 实现订单服务的单体测试 * @author: luds * @version: v1.0 * @date: 2021-03/25 */ public 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); } /** * 用例:追加产品并持久化 */ @Test void shouldAddProduct_thenSaveOrder() { final Order order = spy(OrderProvider.getCreatedOrder()); final Product product = new Product(UUID.randomUUID(), BigDecimal.TEN, "test"); when(orderRepository.findById(order.getId())).thenReturn(Optional.of(order)); tested.addProduct(order.getId(), product); verify(orderRepository).save(order); verify(order).addOrder(product); } /** * 用例:追加产品并抛异常 */ @Test void shouldAddProduct_thenThrowException() { final Product product = new Product(UUID.randomUUID(), BigDecimal.TEN, "test"); final UUID id = UUID.randomUUID(); when(orderRepository.findById(id)).thenReturn(Optional.empty()); final Executable executable = () -> tested.addProduct(id, product); verify(orderRepository, times(0)).save(any(Order.class)); assertThrows(RuntimeException.class, executable); } /** * 用例:完成订单并持久化 */ @Test void shouldCompleteOrder_thenSaveIt() { final Order order = spy(OrderProvider.getCreatedOrder()); when(orderRepository.findById(order.getId())).thenReturn(Optional.of(order)); tested.completeOrder(order.getId()); verify(orderRepository).save(any(Order.class)); verify(order).complete(); } /** * 从订单删除产品并持久化 */ @Test void shouldDeleteProduct_thenSaveOrder() { final Order order = spy(OrderProvider.getCreatedOrder()); final UUID productId = order .getOrderItems() .get(0) .getProductId(); when(orderRepository.findById(order.getId())).thenReturn(Optional.of(order)); tested.deleteProduct(order.getId(), productId); verify(orderRepository).save(order); verify(order).removeOrder(productId); } } ``` # 5.应用层 这里,我们将实现应用程序层。 我们将允许用户通过RESTful API与我们的应用程序进行通信。 因此,让我们创建OrderController: ```java package com.ecommerce.onion.application; import com.ecommerce.onion.application.request.AddProductRequest; import com.ecommerce.onion.application.request.CreateOrderRequest; import com.ecommerce.onion.application.response.OrderResponse; import com.ecommerce.onion.domain.Product; import com.ecommerce.onion.domain.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import java.util.UUID; /** * @packageName: application(应用包) * @className: OrderController(订单控制器) * @description: 围绕此订单领域展开业务 * @author: luds * @version: v1.0 * @date: 2021-04/03 */ @RestController @RequestMapping("/orders") public class OrderController { /** * 订单服务 */ private final OrderService orderService; /** * 由订单服务自动装配订单控制器 * @param orderService */ @Autowired public OrderController(OrderService orderService) { this.orderService = orderService; } /** * 由创建订单请求创建订单,并返回创建订单响应 * @param createOrderRequest 创建订单请求 * @return 返回创建订单响应 */ @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) OrderResponse createOrder(@RequestBody final CreateOrderRequest createOrderRequest) { Product product = createOrderRequest.getProduct(); final UUID id = orderService.createOrder(product); return new OrderResponse(id,product.getId()); } /** * 此订单ID上追加产品请求 * @param id 订单ID * @param addProductRequest 追加产品请求 */ @PostMapping(value = "/{id}/products", consumes = MediaType.APPLICATION_JSON_VALUE) OrderResponse addProduct(@PathVariable final UUID id, @RequestBody final AddProductRequest addProductRequest) { Product product = addProductRequest.getProduct(); orderService.addProduct(id, product); return new OrderResponse(id,product.getId()); } /** * 从此订单ID上删除产品ID * @param id 订单ID * @param productId 产品ID */ @DeleteMapping(value = "/{id}/products", consumes = MediaType.APPLICATION_JSON_VALUE) void deleteProduct(@PathVariable final UUID id, @RequestParam final UUID productId) { orderService.deleteProduct(id, productId); } /** * 完成订单 * @param id 订单ID */ @PostMapping("/{id}/complete") void completeOrder(@PathVariable final UUID id) { orderService.completeOrder(id); } } ``` <订单控制器的结构图(PlantUML)> ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f01f92fa641944f2820a5446e85695c0~tplv-k3u1fbpfcp-watermark.image) 这个简单的Spring Rest控制器**负责协调域逻辑的执行**。 该控制器使外部RESTful接口适应我们的域。 它是通过从OrderService(端口)调用适当的方法来完成的。 # 6.基础设施层 基础结构层包含运行应用程序所需的逻辑。 因此,我们将从创建配置类开始。 首先,让我们实现一个将OrderService注册为Spring bean的类: ```java package com.ecommerce.onion.infrastracture.configuration; import com.ecommerce.onion.DomainLayerApplication; import com.ecommerce.onion.domain.repository.OrderRepository; import com.ecommerce.onion.domain.service.DomainOrderService; import com.ecommerce.onion.domain.service.OrderService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** * @packageName: infrastracture.configuration(基础设施.配置) * @className: BeanConfiguration(bean配置) * @description: 服务配置 * @author: luds * @version: v1.0 * @date: 2021-03/25 */ @Configuration @ComponentScan(basePackageClasses = DomainLayerApplication.class) public class BeanConfiguration { /** * 由订单资料库装配订单服务 * @param orderRepository 订单资料库 * @return 返回订单服务 */ @Bean OrderService orderService(final OrderRepository orderRepository) { return new DomainOrderService(orderRepository); } } ``` 接下来,让我们创建负责启用我们将使用的Spring Data存储库的配置: ```java @EnableMongoRepositories(basePackageClasses = SpringDataMongoOrderRepository.class) public class MongoDBConfiguration { } ``` 我们使用了basePackageClasses属性,因为这些存储库只能位于基础结构层中。 因此,Spring没有理由扫描整个应用程序。 此外,此类可以包含与在MongoDB和我们的应用程序之间建立连接有关的所有内容。 最后,我们将从领域层实现OrderRepository。 我们将在实现中使用SpringDataMongoOrderRepository: ```java package com.ecommerce.onion.infrastracture.repository.mongo; import com.ecommerce.onion.domain.Order; import com.ecommerce.onion.domain.repository.OrderRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import java.util.Optional; import java.util.UUID; /** * @packageName: infrastracture.repository.mongo(基础设施.资料库卡.mongo) * @className: MongoDbOrderRepository(MongoDb订单资料库) * @description: MongoDB存储方式 * @author: luds * @version: v1.0 * @date: 2021-03/26 */ @Component @Primary public class MongoDbOrderRepository implements OrderRepository { /** * 使用SpringData实现的MongoDb订单资料库 */ private final SpringDataMongoOrderRepository orderRepository; /** * SpringData实现的MongoDb资料库来构建MongoDb订单资料库 * @param orderRepository 订单资料库 */ @Autowired public MongoDbOrderRepository(final SpringDataMongoOrderRepository orderRepository) { this.orderRepository = orderRepository; } /** * 根据ID查找订单 * @param id 订单ID * @return 返回订单实例 */ @Override public Optional findById(final UUID id) { return orderRepository.findById(id); } /** * 持久化订单 * @param order 订单实例 */ @Override public void save(final Order order) { orderRepository.save(order); } } ``` 此实现将我们的订单存储在MongoDB中。 在六边形体系结构中,此实现也是适配器。 MongoDB安装请参考:https://juejin.cn/post/6944274331325661215/ # 7.好处 这种方法的第一个优点是我们将**每一层的工作分开**。 我们可以专注于一层而不影响其他层。 此外,它们自然更容易理解,因为它们每个都专注于其逻辑。 另一个很大的优势是,我们已经将领域逻辑与其他所有事物隔离开来。 **领域部分仅包含业务逻辑,可以轻松移至其他环境。** 实际上,让我们更改基础结构层以将Cassandra用作数据库: ```java package com.ecommerce.onion.infrastracture.repository.cassandra; import com.ecommerce.onion.domain.Order; import com.ecommerce.onion.domain.repository.OrderRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Optional; import java.util.UUID; /** * @packageName: infrastracture.repository.cassandra(基础设施.资料库卡.cassandra) * @className: CassandraDbOrderRepository(CassandraDB订单资料库) * @description: CassandraDB存储方式 * @author: luds * @version: v1.0 * @date: 2021-03/26 */ @Component public class CassandraDbOrderRepository implements OrderRepository { /** * 使用SpringData实现的CassandraDB订单资料库 */ private final SpringDataCassandraOrderRepository orderRepository; /** * SpringData实现的CassandraDB资料库来构建CassandraDB订单资料库 * @param orderRepository 订单资料库 */ @Autowired public CassandraDbOrderRepository(SpringDataCassandraOrderRepository orderRepository) { this.orderRepository = orderRepository; } /** * 根据ID查找订单 * @param id 订单ID * @return 返回订单实例 */ @Override public Optional findById(UUID id) { Optional orderEntity = orderRepository.findById(id); if (orderEntity.isPresent()) { return Optional.of(orderEntity.get() .toOrder()); } else { return Optional.empty(); } } /** * 持久化订单 * @param order 订单实例 */ @Override public void save(Order order) { orderRepository.save(new OrderEntity(order)); } } ``` 与MongoDB不同,我们现在使用OrderEntity将领域保留在数据库中。 如果我们在Order域对象中添加特定于技术的注释,则将违反基础结构层和领域层之间的解耦关系。 存储库使领域适应我们的持久化需求。 让我们更进一步,将RESTful应用程序转换为命令行应用程序: ```java package com.ecommerce.onion.application.cli; import com.ecommerce.onion.domain.Product; import com.ecommerce.onion.domain.service.OrderService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.UUID; /** * @packageName: application.cli(应用.客户端包) * @className: CliOrderController(客户端订单控制器) * @description: 围绕此订单领域展开业务 * @author: luds * @version: v1.0 * @date: 2021-03/25 */ @Component public class CliOrderController { private static final Logger LOG = LoggerFactory.getLogger(CliOrderController.class); /** * 订单服务 */ private final OrderService orderService; /** * 由订单服务自动装配订单控制器 * @param 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(); } /** * 创建订单 * @return 返回订单ID */ 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.结论 在本文中,我们学习了如何将与我们的应用程序相关的逻辑分成特定的层。 首先,我们定义了三个主要层:应用程序,领域和基础结构。 之后,我们描述了如何填充它们并解释了优点。 然后,我们提出了每一层的实现: ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/51cf66454f864987af740ae15fee3604~tplv-k3u1fbpfcp-watermark.image) 最后,我们交换了应用程序和基础架构层,而不会影响领域。 完整代码:https://gitee.com/actual-combat-study/system-architecture/tree/master/ecommerce-backend 参考:https://www.baeldung.com/hexagonal-architecture-ddd-spring 延伸阅读:https://resourceful-badger-85476-dev-ed.my.salesforce.com/home/home.jsp */ 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); } /** * 用例:追加产品并持久化 */ @Test void shouldAddProduct_thenSaveOrder() { final Order order = spy(OrderProvider.getCreatedOrder()); final Product product = new Product(UUID.randomUUID(), BigDecimal.TEN, "test"); when(orderRepository.findById(order.getId())).thenReturn(Optional.of(order)); tested.addProduct(order.getId(), product); verify(orderRepository).save(order); verify(order).addOrder(product); } /** * 用例:追加产品并抛异常 */ @Test void shouldAddProduct_thenThrowException() { final Product product = new Product(UUID.randomUUID(), BigDecimal.TEN, "test"); final UUID id = UUID.randomUUID(); when(orderRepository.findById(id)).thenReturn(Optional.empty()); final Executable executable = () -> tested.addProduct(id, product); verify(orderRepository, times(0)).save(any(Order.class)); assertThrows(RuntimeException.class, executable); } /** * 用例:完成订单并持久化 */ @Test void shouldCompleteOrder_thenSaveIt() { final Order order = spy(OrderProvider.getCreatedOrder()); when(orderRepository.findById(order.getId())).thenReturn(Optional.of(order)); tested.completeOrder(order.getId()); verify(orderRepository).save(any(Order.class)); verify(order).complete(); } /** * 从订单删除产品并持久化 */ @Test void shouldDeleteProduct_thenSaveOrder() { final Order order = spy(OrderProvider.getCreatedOrder()); final UUID productId = order .getOrderItems() .get(0) .getProductId(); when(orderRepository.findById(order.getId())).thenReturn(Optional.of(order)); tested.deleteProduct(order.getId(), productId); verify(orderRepository).save(order); verify(order).removeOrder(productId); } } ``` # 5.应用层 这里,我们将实现应用程序层。 我们将允许用户通过RESTful API与我们的应用程序进行通信。 因此,让我们创建OrderController: ​```java package com.ecommerce.onion.application; import com.ecommerce.onion.application.request.AddProductRequest; import com.ecommerce.onion.application.request.CreateOrderRequest; import com.ecommerce.onion.application.response.CreateOrderResponse; import com.ecommerce.onion.domain.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import java.util.UUID; /** * @packageName: application(应用包) * @className: OrderController(订单控制器) * @description: 围绕此订单领域展开业务 * @author: luds * @version: v1.0 * @date: 2021-03/25 */ @RestController @RequestMapping("/orders") public class OrderController { /** * 订单服务 */ private final OrderService orderService; /** * 由订单服务自动装配订单控制器 * @param orderService */ @Autowired public OrderController(OrderService orderService) { this.orderService = orderService; } /** * 由创建订单请求创建订单,并返回创建订单响应 * @param createOrderRequest 创建订单请求 * @return 返回创建订单响应 */ @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) CreateOrderResponse createOrder(@RequestBody final CreateOrderRequest createOrderRequest) { final UUID id = orderService.createOrder(createOrderRequest.getProduct()); return new CreateOrderResponse(id); } /** * 此订单ID上追加产品请求 * @param id 订单ID * @param addProductRequest 追加产品请求 */ @PostMapping(value = "/{id}/products", consumes = MediaType.APPLICATION_JSON_VALUE) void addProduct(@PathVariable final UUID id, @RequestBody final AddProductRequest addProductRequest) { orderService.addProduct(id, addProductRequest.getProduct()); } /** * 从此订单ID上删除产品ID * @param id 订单ID * @param productId 产品ID */ @DeleteMapping(value = "/{id}/products", consumes = MediaType.APPLICATION_JSON_VALUE) void deleteProduct(@PathVariable final UUID id, @RequestParam final UUID productId) { orderService.deleteProduct(id, productId); } /** * 完成订单 * @param id 订单ID */ @PostMapping("/{id}/complete") void completeOrder(@PathVariable final UUID id) { orderService.completeOrder(id); } } ``` 这个简单的Spring Rest控制器**负责协调域逻辑的执行**。 该控制器使外部RESTful接口适应我们的域。 它是通过从OrderService(端口)调用适当的方法来完成的。 # 6.基础设施层 基础结构层包含运行应用程序所需的逻辑。 因此,我们将从创建配置类开始。 首先,让我们实现一个将OrderService注册为Spring bean的类: ```java package com.ecommerce.onion.infrastracture.configuration; import com.ecommerce.onion.DomainLayerApplication; import com.ecommerce.onion.domain.repository.OrderRepository; import com.ecommerce.onion.domain.service.DomainOrderService; import com.ecommerce.onion.domain.service.OrderService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** * @packageName: infrastracture.configuration(基础设施.配置) * @className: BeanConfiguration(bean配置) * @description: 服务配置 * @author: luds * @version: v1.0 * @date: 2021-03/25 */ @Configuration @ComponentScan(basePackageClasses = DomainLayerApplication.class) public class BeanConfiguration { /** * 由订单资料库装配订单服务 * @param orderRepository 订单资料库 * @return 返回订单服务 */ @Bean OrderService orderService(final OrderRepository orderRepository) { return new DomainOrderService(orderRepository); } } ``` 接下来,让我们创建负责启用我们将使用的Spring Data存储库的配置: ```java @EnableMongoRepositories(basePackageClasses = SpringDataMongoOrderRepository.class) public class MongoDBConfiguration { } ``` 我们使用了basePackageClasses属性,因为这些存储库只能位于基础结构层中。 因此,Spring没有理由扫描整个应用程序。 此外,此类可以包含与在MongoDB和我们的应用程序之间建立连接有关的所有内容。 最后,我们将从领域层实现OrderRepository。 我们将在实现中使用SpringDataMongoOrderRepository: ```java package com.ecommerce.onion.infrastracture.repository.mongo; import com.ecommerce.onion.domain.Order; import com.ecommerce.onion.domain.repository.OrderRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import java.util.Optional; import java.util.UUID; /** * @packageName: infrastracture.repository.mongo(基础设施.资料库卡.mongo) * @className: MongoDbOrderRepository(MongoDb订单资料库) * @description: MongoDB存储方式 * @author: luds * @version: v1.0 * @date: 2021-03/26 */ @Component @Primary public class MongoDbOrderRepository implements OrderRepository { /** * 使用SpringData实现的MongoDb订单资料库 */ private final SpringDataMongoOrderRepository orderRepository; /** * SpringData实现的MongoDb资料库来构建MongoDb订单资料库 * @param orderRepository 订单资料库 */ @Autowired public MongoDbOrderRepository(final SpringDataMongoOrderRepository orderRepository) { this.orderRepository = orderRepository; } /** * 根据ID查找订单 * @param id 订单ID * @return 返回订单实例 */ @Override public Optional findById(final UUID id) { return orderRepository.findById(id); } /** * 持久化订单 * @param order 订单实例 */ @Override public void save(final Order order) { orderRepository.save(order); } } ``` 此实现将我们的订单存储在MongoDB中。 在六边形体系结构中,此实现也是适配器。 # 7.好处 这种方法的第一个优点是我们将**每一层的工作分开**。 我们可以专注于一层而不影响其他层。 此外,它们自然更容易理解,因为它们每个都专注于其逻辑。 另一个很大的优势是,我们已经将领域逻辑与其他所有事物隔离开来。 **领域部分仅包含业务逻辑,可以轻松移至其他环境。** 实际上,让我们更改基础结构层以将Cassandra用作数据库: ```java package com.ecommerce.onion.infrastracture.repository.cassandra; import com.ecommerce.onion.domain.Order; import com.ecommerce.onion.domain.repository.OrderRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Optional; import java.util.UUID; /** * @packageName: infrastracture.repository.cassandra(基础设施.资料库卡.cassandra) * @className: CassandraDbOrderRepository(CassandraDB订单资料库) * @description: CassandraDB存储方式 * @author: luds * @version: v1.0 * @date: 2021-03/26 */ @Component public class CassandraDbOrderRepository implements OrderRepository { /** * 使用SpringData实现的CassandraDB订单资料库 */ private final SpringDataCassandraOrderRepository orderRepository; /** * SpringData实现的CassandraDB资料库来构建CassandraDB订单资料库 * @param orderRepository 订单资料库 */ @Autowired public CassandraDbOrderRepository(SpringDataCassandraOrderRepository orderRepository) { this.orderRepository = orderRepository; } /** * 根据ID查找订单 * @param id 订单ID * @return 返回订单实例 */ @Override public Optional findById(UUID id) { Optional orderEntity = orderRepository.findById(id); if (orderEntity.isPresent()) { return Optional.of(orderEntity.get() .toOrder()); } else { return Optional.empty(); } } /** * 持久化订单 * @param order 订单实例 */ @Override public void save(Order order) { orderRepository.save(new OrderEntity(order)); } } ``` 与MongoDB不同,我们现在使用OrderEntity将领域保留在数据库中。 如果我们在Order域对象中添加特定于技术的注释,则将违反基础结构层和领域层之间的解耦关系。 存储库使领域适应我们的持久化需求。 让我们更进一步,将RESTful应用程序转换为命令行应用程序: ```java package com.ecommerce.onion.application.cli; import com.ecommerce.onion.domain.Product; import com.ecommerce.onion.domain.service.OrderService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.UUID; /** * @packageName: application.cli(应用.客户端包) * @className: CliOrderController(客户端订单控制器) * @description: 围绕此订单领域展开业务 * @author: luds * @version: v1.0 * @date: 2021-03/25 */ @Component public class CliOrderController { private static final Logger LOG = LoggerFactory.getLogger(CliOrderController.class); /** * 订单服务 */ private final OrderService orderService; /** * 由订单服务自动装配订单控制器 * @param 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(); } /** * 创建订单 * @return 返回订单ID */ 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.结论 在本文中,我们学习了如何将与我们的应用程序相关的逻辑分成特定的层。 首先,我们定义了三个主要层:应用程序,领域和基础结构。 之后,我们描述了如何填充它们并解释了优点。 然后,我们提出了每一层的实现: ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/51cf66454f864987af740ae15fee3604~tplv-k3u1fbpfcp-watermark.image) 最后,我们交换了应用程序和基础架构层,而不会影响领域。 # 9. 样例 ## 9.1 创建订单 http://localhost:8080/orders/ **request** | HTTP request methods | HTTP headers | HTTP Body | | -------------------- | ----------------------------- | ------------------------------------------------------------ | | POST | Content-Type:application/json | {
"product": {
"price": "100",
"name": "水杯"
}
} | **response** | Status | Body | | ------ | ------------------------------------------------------------ | | 200 | {
"orderId": "01860992-89d3-46ef-a360-690438a1cb6d",
"productId": "32671125-66b1-4a3e-b8d9-cdb778ce428a"
} | ## 9.2 已有订单基础上,追加产品 http://localhost:8080/orders/01860992-89d3-46ef-a360-690438a1cb6d/products **request** | HTTP request methods | HTTP headers | HTTP Body | | -------------------- | ----------------------------- | ------------------------------------------------------------ | | POST | Content-Type:application/json | {
"product": {
"price": "120",
"name": "水杯"
}
} | **response** | Status | Body | | ------ | ------------------------------------------------------------ | | 200 | {
"orderId": "01860992-89d3-46ef-a360-690438a1cb6d",
"productId": "5754432d-13ac-48d9-97e7-ee1a6fb4baee"
} | ## 9.3 从已有订单中,删除产品 http://localhost:8080/orders/01860992-89d3-46ef-a360-690438a1cb6d/products?productId=32671125-66b1-4a3e-b8d9-cdb778ce428a **request** | HTTP request methods | HTTP headers | HTTP Body | | -------------------- | ----------------------------- | --------- | | DELETE | Content-Type:application/json | | **response** | Status | Body | | ------ | ---- | | 200 | | ## 9.4 完成订单 http://localhost:8080/orders/01860992-89d3-46ef-a360-690438a1cb6d/complete **request** | HTTP request methods | HTTP headers | HTTP Body | | -------------------- | ----------------------------- | --------- | | POST | Content-Type:application/json | | **response** | Status | Body | | ------ | ---- | | 200 | | Status:200之外的值请参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status # 10. 追加swagger功能 访问地址:http://localhost:8080/swagger-ui/index.html?urls.primaryName 可以选择:前台和后台