本文主要研究一下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


codecraft
11.9k 声望2k 粉丝

当一个代码的工匠回首往事时,不因虚度年华而悔恨,也不因碌碌无为而羞愧,这样,当他老的时候,可以很自豪告诉世人,我曾经将代码注入生命去打造互联网的浪潮之巅,那是个很疯狂的时代,我在一波波的浪潮上留下...


引用和评论

0 条评论