聊聊Tomato Architecture

本文主要研究一下Tomato Architecture

Clean/Onion/Hexagonal/Ports&Adapters Architectures

Clean Architecture

clean architecture定义了四层结构,最内层是entities(enterprise business rules),再往外是use cases(application business rules),接着是interface adapters(比如controller、presenters、gateways),最外层是frameworks & drivers(比如web、ui、db、devices、external interfaces)

clean architecture主要是分了4层结构,domain层,有的会把repository接口放在这一层,然后domain service会调用repository;use case层对应ddd的application层,主要是业务编排,有的也把repository接口放在这一层;interfaces adapters层会对输入和输出进行适配,实现use case定义的方法,类似ddd的interfaces层;infrastructure层主要是对基础服务/类库的管理,有些工程把对repository的实现也放这里了,貌似不太妥当。

Onion Architecture

Onion Architecture定义了domain、repository、services、ui这几层,其核心要点如下:

  • 整个应用基于独立的domain构建
  • 内部的layer定义接口,外部的layer实现接口
  • 内层与外层通过接口解耦
  • services(business logic)可以独立于infrastructure编译和运行

Onion Architecture的核心在于内层定义接口,外层来进行实现,然后业务逻辑层则是基于接口来实现业务逻辑,基于接口来进行解耦。

Hexagonal Architecture(Ports&Adapters Architecture)

Ports and Adapters architecture,又叫Hexagonal architecture,其中ports层是六边形的边界,其中port又可以分为driver port及driven port,简单理解对应输入层及输出层;边界保护的是内部的app,其中app包括use cases或者叫做application services层以及domain层;adapter可以理解为将外部依赖进行适配,实现port层定义的接口。

buckpal工程分了adapter、application、domain三层;其中application层定义了port包,该包定义了in、out两种类型的接口;adapter层也分in、out两类,分别实现application/port层的接口;application的service则实现了port的接口。其中domain层不依赖任何层;application层的port定义了接口,然后service层实现接口和引用接口;adapter层则实现了application的port层的接口。

Tomato Architecture

既往架构评论

There is No Silver Bullet Architecture

Clean / Onion / Hexagonal / Ports&Adapters Architectures都不能解决所有问题

Wrong pursuit of Testability

想通过这些架构的抽象来使得单元测试不需要依赖外部服务(数据库、MQ、定时任务等)有点不接地气,现实的企业级服务代码经常是重度依赖这些外部服务的,而且即使是这么做,无论有多少单元测试在没有集成测试的时候也没没有信心保证代码没有问题

Tomato Architecture倾向于移除这些抽象,直接写更多的集成测试来保证代码的质量

Simplicity and Readability Wins in the Long Run

简单才是王道,这样子后续可维护性更强

Key Principles

  • 思考哪些适合自己而不是盲从大众的建议
  • 保持简单而不是过度设计
  • 直接开干,没有必要不过度抽象想着替换
  • 确保方案在整体而不是局部适用

Architecture Diagram

Implementation Guidelines

Package by feature

对于构建单体或者模块化单体,强烈建议通过feature来分包,而不是技术层面的分层

Keep "Application Core" independent of delivery mechanism (Web, Scheduler Jobs, CLI)

Application Core中剥离Web, Scheduler Jobs, CLI这些服务提供方式

Separate the business logic execution from input sources (Web Controllers, Message Listeners, Scheduled Jobs etc)

Web Controllers, Message Listeners, Scheduled Jobs这些层不要包含业务逻辑

不建议:

复制代码
@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);
   }
}

Don't let the "External Service Integrations" influence the "Application Core" too much

尽可能少让外部服务代码渗透到Application Core逻辑中,确保Application Core尽可能少依赖这些外部服务

不建议

复制代码
@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 ...;
   }
}

Keep domain logic in domain objects

尽可能把一些领域逻辑封装在domain objects中,比如不建议:

复制代码
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();
      ...
   }
}

No unnecessary interfaces

不要一上来就定义接口,以期望说哪天会有其他实现,如果是说方便单测,但事实上现在有很多框架可以进行mock,可以不需要定义接口。所以如无必要不定义接口。

Embrace the framework's power and flexibility

可以直接拥抱框架,不需要在这之前试图再抽象一层以图说后续要切换,一般这类需求不多,直接用框架的能力就好

Test not only units, but whole features

通过mock去跑单元测试是有必要,但是它没办法验证替代集成测试,所以借助注入testcontainers来直接进行集成测试更能提升对代码的信心

小结

Tomato Architecture在实践的基础上对Clean/Onion/Hexagonal/Ports&Adapters Architectures进行了改良,大的原则还是不变,即interface层不渗透到业务层,业务层代码也不泄露到interface层。

改良的部分是:

  • 业务层尽量少依赖外部服务层
  • 如无多个实现则少定义接口,利用框架能力进行mock
  • 少在框架层上进行抽象以试图后续切换,一般大的框架比较少有切换需求
  • 单元测试不如集成测试实在

doc

相关推荐
xiaoqiMikko1 分钟前
Central 上的 Spring 5.3.41 不是官方发的:逐字节比对,官方修两条它少一条
java·安全
用户094248568031 分钟前
第8章:Java 内存模型(JMM)入门——可见性、有序性、原子性
java·jvm
AI深栈2 分钟前
第 9 章 · Model Context Protocol(MCP)
java·人工智能
雨辰AI4 分钟前
信创多租户项目 9 大踩坑|数据隔离失效、权限越权终极解决(金仓 / 达梦 / 高斯全库适配)
java·大数据·数据库·后端
captain3764 分钟前
网络原理(4)-TCP ▲▲▲
java·服务器·网络·网络协议·tcp/ip
橙子圆1239 分钟前
JUC之线程和进程
java
Bs_MoneyMagnet13 分钟前
基于springboot+vue的旅游行程分享与推荐小程序的设计与实现 源码+文档
java·vue.js·spring boot·后端·微信小程序·毕业设计·计算机毕业设计
一技安身17 分钟前
【信创】统信UOS 银河麒麟离线部署Python3.11完整方案
android·java·python3.11
陈皮波比茶31 分钟前
k8s 快速入门
java·kubelet
Json____38 分钟前
基于 Spring Boot + Vue3 的校园自习室座位预约系统实战
java·管理系统·it学习·wwwoop.com