知識庫 / Spring RSS 訂閱

使用六角架構、DDD 和 Spring 組織層級結構

Architecture,Spring
HongKong
3
01:04 PM · Dec 06 ,2025

1. 概述

在本教程中,我們將使用 DDD(領域驅動設計)實施一個 Spring 應用。此外,我們還將使用六角架構組織層級。

通過這種方法,我們可以輕鬆地更換應用程序的不同層級。

2. 六角架構

六角架構是一種設計軟件應用程序的方法,該方法圍繞領域邏輯進行設計,以將其與外部因素隔離。

領域邏輯位於業務核心中(我們稱之為內部部分),其餘部分則為外部部分。 通過端口和適配器訪問領域邏輯。

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
}

這是我們的聚合根。所有與業務邏輯相關的操作都將通過該類處理。此外,訂單負責保持自身處於正確狀態:

  • 訂單隻能通過給定 ID 和一個產品創建,構造函數本身也會使用CREATED狀態初始化訂單。
  • 一旦訂單完成,訂單項的修改將不可能。
  • 無法通過外部域對象(如設置器)來更改訂單

此外,訂單類還負責創建其訂單項

讓我們創建訂單項類:

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

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

    // getters
}

如我們所見,OrderItem 是基於 Product 創建的。它保留了對該產品的引用,並存儲了當前的價格。

接下來,我們將創建一個倉庫接口(在六角架構中稱為 port)。接口的實現將位於基礎設施層:

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

    void save(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 在基礎設施層中對其進行 wire。

由於領域層完全與應用程序和基礎設施層解耦我們可以也獨立對其進行測試:

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. 應用層

在這一部分,我們將實現應用層。我們將允許用户通過 RESTful API 與我們的應用程序進行通信。

讓我們創建 <em >OrderController:</em >

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

這個簡單的 Spring Rest 控制器負責協調領域邏輯的執行

該控制器通過調用 OrderService (端口)中的相應方法,將外部 RESTful 接口適配到我們的領域。

6. 基礎設施層

基礎設施層包含運行應用程序所需的邏輯。

我們首先將創建配置類。首先,我們將實現一個用於將 OrderService 註冊為 Spring Bean 的類:

@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. 優勢

我們採用這種方法的主要優勢在於,我們將工作分解到每一層。 這樣,我們可以專注於一層,而不會影響其他層。

此外,由於每一層都專注於其自身的邏輯,因此它們更容易理解。

另一個重要的優勢是,我們已經將領域邏輯與所有其他內容隔離。 領域部分僅包含業務邏輯,並且可以輕鬆地移動到不同的環境

事實上,讓我們將基礎設施層更改為使用 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. 結論

在本文中,我們學習瞭如何將與我們的應用程序相關的邏輯分解為特定的層。

首先,我們定義了三個主要層:應用程序層、領域層和基礎設施層。然後,我們描述瞭如何填充這些層,並解釋了其優勢。

接下來,我們為每層設計了實現方案:

最後,我們交換了應用程序層和基礎設施層,而沒有影響領域層。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.