本文主要研究一下Tomato 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定义了domain、repository、services、ui这几层,其核心要点如下:
Onion 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层的接口。
Clean / Onion / Hexagonal / Ports&Adapters Architectures都不能解决所有问题
想通过这些架构的抽象来使得单元测试不需要依赖外部服务(数据库、MQ、定时任务等)有点不接地气,现实的企业级服务代码经常是重度依赖这些外部服务的,而且即使是这么做,无论有多少单元测试在没有集成测试的时候也没没有信心保证代码没有问题 Tomato Architecture倾向于移除这些抽象,直接写更多的集成测试来保证代码的质量
简单才是王道,这样子后续可维护性更强
对于构建单体或者模块化单体,强烈建议通过feature来分包,而不是技术层面的分层
Application Core中剥离Web, Scheduler Jobs, CLI这些服务提供方式
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);
}
}
尽可能少让外部服务代码渗透到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 ...;
}
}
尽可能把一些领域逻辑封装在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();
...
}
}
不要一上来就定义接口,以期望说哪天会有其他实现,如果是说方便单测,但事实上现在有很多框架可以进行mock,可以不需要定义接口。所以如无必要不定义接口。
可以直接拥抱框架,不需要在这之前试图再抽象一层以图说后续要切换,一般这类需求不多,直接用框架的能力就好
通过mock去跑单元测试是有必要,但是它没办法验证替代集成测试,所以借助注入testcontainers来直接进行集成测试更能提升对代码的信心
Tomato Architecture在实践的基础上对Clean/Onion/Hexagonal/Ports&Adapters Architectures进行了改良,大的原则还是不变,即interface层不渗透到业务层,业务层代码也不泄露到interface层。
改良的部分是: