天天看点

番茄架构:最务实的架构方案,以Spring Boot示例

作者:爱科学的卫斯理
番茄架构:最务实的架构方案,以Spring Boot示例

番茄架构

番茄架构是一种遵循常识宣言的软件架构方法。

1、常识宣言

  • 想想什么对你的软件是最好的,而不是盲目地听从大众的建议。
  • 努力让事情变得简单,而不是通过猜测未来十年的需求来过度设计解决方案。
  • 进行研究和开发,选择一种技术并接受它,而不是在想着创建实现能够被替代的抽象。
  • 确保你的解决方案是一个整体,而不是单个的单元。

2、架构图

番茄架构:最务实的架构方案,以Spring Boot示例

番茄架构图

3、实现指南

3.1 按功能分包

将代码组织成包的常见模式是基于技术层(如控制器层、服务层、存储库层等)进行拆分。如果你正在构建一个已经专注于特定模块或业务功能的微服务,那么这种方法可能很好。

但如果你正在构建一个单体或者是模块化单体的时候,那么强烈建议首先要按功能而不是技术层进行包的拆分。

3.2 保持“应用核心”(Application Core)不依赖于交付机制(Web、调度任务、命令行)

“应用核心”应该暴露出可以从main() 方法直接可以调用的API。为了实现这一点,“应用核心”不应该依赖于它的调用上下文。这意味着“应用核心”不应该依赖于任何HTTP/Web层库。类似地,如果你的“应用核心”正在被调度任务或命令行所使用,那么任何调度逻辑或命令执行的逻辑都不应该污染泄漏到“应用核心”中。

3.3 将业务逻辑执行与输入源(Web控制器、消息监听器和调度任务等)分开

输入源(如Web控制器、消息监听器、计划作业等)应该是一个薄层,从请求中提取数据,并将实际的业务逻辑执行委托给“应用核心”。

不要这样做

@RestController
class CustomerController {
    private final CustomerService customerService;
    
    @PostMapping("/api/customers")
    void createCustomer(@RequestBody Customer customer) {
       if(customerService.existsByEmail(customer.getEmail())) {
           throw new EmailAlreadyInUseException(customer.getEmail());
       }
       customer.setCreateAt(Instant.now());
       customerService.save(customer);
    }
}           

相反,要这样做

@RestController
class CustomerController {
    private final CustomerService customerService;
    
    @PostMapping("/api/customers")
    void createCustomer(@RequestBody Customer customer) {
       customerService.save(customer);
    }
}

@Service
@Transactional
class CustomerService {
   private final CustomerRepository customerRepository;

   void save(Customer customer) {
      if(customerRepository.existsByEmail(customer.getEmail())) {
         throw new EmailAlreadyInUseException(customer.getEmail());
      }
      customer.setCreateAt(Instant.now());
      customerRepository.save(customer);
   }
}           

使用这种方式,无论你是尝试从REST API调用还是从命令行创建Customer,所有业务逻辑都集中在“应用核心”中。

不要这样做

@Component
class OrderProcessingJob {
    private final OrderService orderService;
    
    @Scheduled(cron="0 * * * * *")
    void run() {
       List<Order> orders = orderService.findPendingOrders();
       for(Order order : orders) {
           this.processOrder(order);
       }
    }
    
    private void processOrder(Order order) {
       ...
       ...
    }
}           

相反,要这样做

@Component
class OrderProcessingJob {
   private final OrderService orderService;

   @Scheduled(cron="0 * * * * *")
   void run() {
      List<Order> orders = orderService.findPendingOrders();
      orderService.processOrders(orders);
   }
}

@Service
@Transactional
class OrderService {

   public void processOrders(List<Order> orders) {
       ...
       ...
   }
}           

使用这种方式,你可以将订单处理逻辑与调度程序解耦,并且可以进行独立测试,而无需通过调度程序来触发。

不要让“外部服务集成”过多影响“应用核心”

从“应用核心”,我们可能需要与数据库、消息中间件或第三方web服务等进行通信。必须要小心是,业务逻辑的执行者不能严重依赖于“外部服务集成”。

假设你正在使用Spring Data JPA进行持久化,并且你希望使用分页从CustomerService 获取customers。

不要这样做

@Service
@Transactional
class CustomerService {
   private final CustomerRepository customerRepository;

   PagedResult<Customer> getCustomers(Integer pageNo) {
      Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE, Sort.of("name"));
      Page<Customer> cusomersPage = customerRepository.findAll(pageable);
      return convertToPagedResult(cusomersPage);
   }
}           

相反,要这样做

@Service
@Transactional
class CustomerService {
   private final CustomerRepository customerRepository;

   PagedResult<Customer> getCustomers(Integer pageNo) {
      return customerRepository.findAll(pageNo);
   }
}

@Repository
class JpaCustomerRepository {

   PagedResult<Customer> findAll(Integer pageNo) {
      Pageable pageable = PageRequest.of(pageNo, PAGE_SIZE, Sort.of("name"));
      return ...;
   }
}           

这样,任何持久性库的更改只会影响存储库层。

3.5 将领域逻辑保留在领域对象中

如果你有领域对象的状态改变类的方法,或者需要从对象中计算一些东西的方法,那么这些方法都属于该领域的对象。

不要这样做

class Cart {
    List<LineItem> items;
}

@Service
@Transactional
class CartService {

   CartDTO getCart(UUID cartId) {
      Cart cart = cartRepository.getCart(cartId);
      BigDecimal cartTotal = this.calculateCartTotal(cart);
      ...
   }
   
   private BigDecimal calculateCartTotal(Cart cart) {
      ...
   }
}           

相反,要这样做

class Cart {
    List<LineItem> items;

   public BigDecimal getTotal() {
      ...
   }
}

@Service
@Transactional
class CartService {

   CartDTO getCart(UUID cartId) {
      Cart cart = cartRepository.getCart(cartId);
      BigDecimal cartTotal = cart.getTotal();
      ...
   }
}           

3.6 不要非必要的接口

不要在一开始创建接口时就希望有一天我们可以为这个接口添加另一个实现。如果这一天真的到来,我们现在有的强大的IDE,只需按几下键就可以提取这个接口。

如果创建接口的原因是为了使用Mock实现进行测试,那么我们有Mock库,如Mockito,它能够在不实现接口的情况下模拟类。

所以,除非有很好的理由,否则不要创建接口。

3.7 拥抱框架的强大和灵活性

通常,创建库和框架是为了满足大多数应用程序所需的公共需求。所以,当你选择一个库/框架来更快地构建你的应用程序时,你应该拥抱它。

如果我们不利用所选框架提供的功能和灵活性,而是在所选框架之上创建一个间接或抽象,并希望有一天你可以切换到另一个框架,这样做通常是一个非常糟糕的主意。

例如,Spring框架为处理数据库事务、缓存、方法级安全等提供了声明性的注解支持。引入我们自己的类似注解并通过将实际处理委托给框架来重新实现相同的功能支持是完全不必要的。

相反,最好是直接使用框架提供的注解,或者根据需要将注解组合在一起形成我们需要的新的语意的注解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transactional
public @interface UseCase {
   @AliasFor(
        annotation = Transactional.class
   )
   Propagation propagation() default Propagation.REQUIRED;
}           

3.8 不仅要测试单元,还要测试整个功能

我们当然需要通过模拟外部依赖项来编写单元测试来测试这些单元(业务逻辑)。 但更重要的是我们需要验证整体功能是否正常工作。

就算我们的单元测试以毫秒为单位运行,我们是否就可以放心地投入生产?当然不会。 我们应该通过测试实际的外部依赖项(例如数据库或消息代理)来验证整个功能是否正常工作。 这才会让我们更有信心。

“我们应该拥有完全独立于外部依赖的核心领域(Core Domain)”的这个理念,来自于使用真正的依赖项进行测试非常具有挑战性或根本不可能的时代。

幸运的是,我们现在有更好的技术(例如:Testcontainers)来测试真实的依赖项。 使用真实依赖项进行测试可能会花费更多时间,但与收益相比,这是可以忽略不计的成本。

FAQs

  1. "番茄"这个名字是什么意思?

如果你认为“六边形架构”的6条边是没有特别意义的,那么你应该可以接受“番茄”。毕竟,我们还有洋葱架构,为什么没有番茄架构呢? :-)

  1. 如果别人因为我遵循这个架构而叫我“代码猴子”(Code monkey)怎么办?

忽略它们。专注于交付业务价值。

"Code monkey" 是一个俚语,用于形容一个编程员或程序员,特指那些专注于编写代码、进行重复性工作、缺乏创造性和独立思考的人。它有时带有一些贬义或轻蔑的含义,暗指这些人只是机械地按照规定的指令编写代码,而缺乏深入理解和创新能力。

原文地址:https://github.com/sivaprasadreddy/tomato-architecture

继续阅读