在六边形架构中使用端口/适配器的具体好处是什么?

问题描述 投票:0回答:1

假设我们有一个端口/适配器模式的典型实现:

@RestController
private class OrderController {

    private final CreateOrderPort createOrderPort;
    
    @PostMapping
    void createOrder(String customerId) {
        createOrderPort.createOrder(customerId);
    }
}


private class KafkaConsumer {

    private final CreateOrderPort createOrderPort;
    
    @PostMapping
    public void createOrder(CreateOrderEvent event) {
        createOrderPort.createOrder(event.getCustomerId());
    }
}

public interface CreateOrderPort {
    void createOrder(String customerId);
}

@Service
public class CreateOrderService implements CreateOrderPort {
    public void createOrder(String customerId){
        orderRepository.save(new Order(customerId));
    }
}

我只是想知道只有一个界面的确切好处是什么? 如果Kafka消费者和控制器直接使用CreateOrderService会有什么区别?

@RestController
private class OrderController {

    private final CreateOrderService createOrderService;
    
    @PostMapping
    void createOrder(String customerId) {
        createOrderService.createOrder(customerId);
    }
}


private class KafkaConsumer {

    private final CreateOrderService createOrderService;
    
    @PostMapping
    public void createOrder(CreateOrderEvent event) {
        createOrderService.createOrder(event.getCustomerId());
    }
}

@Service
public class CreateOrderService{
    public void createOrder(String customerId){
        orderRepository.save(new Order(customerId));
    }
}

我们仍然将技术细节(适配器或边界)和业务细节(核心或领域)分开。如果服务发生变化,就会对技术层产生任何影响。

java spring-boot port hexagonal-architecture
1个回答
0
投票

我只是想知道只有一个界面的确切好处是什么?如果Kafka消费者和控制器直接使用CreateOrderService会有什么区别?

您可以更轻松地测试它,因为创建模拟所需的精力更少。

class OrderControllerTest {

    private String customerId;

    @Test
    void createOrder() {
        CreateOrderPort createOrderPortMock = customerId -> OrderControllerTest.this.customerId = customerId;
        OrderController orderController = new OrderController(createOrderPortMock);

        orderController.createOrder("1234");

        assertEquals("1234", customerId);
    }
}

在更复杂的场景中,您可能想要创建一个实现输入端口的模拟类 或者您可能想创建一个专门的类以提高可读性。

class OrderControllerTest {

    class CreateOrderPortMock implements CreateOrderPort {

        private String customerId;

        @Override
        public void createOrder(String customerId) {
            this.customerId = customerId;
        }

        public void assertCreateOrderInvoked(String expectedCustomerId){
            assertEquals(expectedCustomerId, this.customerId);
        }
    }



    @Test
    void createOrder() {
        CreateOrderPortMock createOrderPortMock = new CreateOrderPortMock();
        OrderController orderController = new OrderController(createOrderPortMock);

        orderController.createOrder("1234");

        createOrderPortMock.assertCreateOrderInvoked("1234");
    }
}

当然,有人可能会争辩说,您也可以扩展实际的实现并重写该方法。但是这个 可能会很棘手,因为真正的实现需要构造函数参数才能创建。什么时候 你使用真正的实现,你必须处理真正的实现依赖关系。

public class CreateOrderService implements CreateOrderPort {

    private OrderRepository orderRepository;

    public CreateOrderService(OrderRepository orderRepository) {
        this.orderRepository = Objects.requireNonNull(orderRepository);
    }

    public void createOrder(String customerId){
        orderRepository.save(new Order(customerId));
    }
}

如果真正的实现确实检查构造函数或执行一些逻辑,则需要满足所有这些 创建模拟以覆盖 createOrder 方法的先决条件。如果您这样做,您的测试就会受到限制 所有这些先决条件。这意味着即使它与它所做的事情无关,它也会破产 它打破了。例如。如果有人更改构造函数逻辑,您的“创建订单控制器”测试就会中断。

最后,如果您使用接口,则不需要使用 cglib 代理或其他任何东西的花哨的模拟框架 只是模拟简单的输入端口。因此,如果更新某些库(和传递依赖项),您的依赖项就会减少,麻烦也会减少。

© www.soinside.com 2019 - 2024. All rights reserved.