Alibaba COLA 4.0 架构实践
Alibaba COLA架构 4.0
COLA 是 Clean Object-Oriented and Layered Architecture的缩写,代表“整洁面向对象分层架构”。 目前COLA已经发展到COLA v4。
互联网业务项目一般会遇到如下一些普遍问题:
虽然整体架构规划做的不错,但落地严重偏离,缺乏足够的抽象和弹性设计,面向流程编程。
业务的工期紧、迭代快,导致代码结构混乱,几乎没有代码注释和文档,即使有项目代码规范。
人员变动频繁,接手别人的老项目,新人根本没时间吃透代码结构,也很难快速了解上下文,紧迫的工期又只能让屎山越堆越大。
多人协作开发,每个人的编码习惯不同,工具类代码各用个的,业务命名也经常冲突,团队成员庞大后更加影响效率。
看似相同的功能,却很难加入改动,却经常听到:要写这张卡,先把之前的哪哪改了。
Code Review效果不佳,很难快速了解别人的上下文,只能简单看到一些命名、设计原则或明显的实现问题。
大部分团队几乎没有时间做代码重构,任由代码腐烂。或者没有动力或KPI进行代码重构。
不写单元测试,或编写的大量单元测试用处不大,有新功能加入或重构时导致要修改大量的测试。
每当新启动一个代码仓库,都是信心满满,结构整洁。但是时间越往后,代码就变得腐败不堪,技术债务越来越庞大…
有无好的解决方案呢?也是有的:
设计完善的应用架构以及代码落地规范,定期进行Review,让代码的腐烂来得慢一些。(当然很难做到完全不腐烂)
定期做代码重构,解决技术债务
设计尽量保持简单,让不同层级的开发都能快速看懂并上手开发,而不是在一堆复杂的没人看懂的代码上堆更多的屎山。
设计尽量遵守SOLID原则,开发人员经常违反单一职责原则和开闭原则,导致代码和测试调整困难。
坚持Code Review,先看模型设计,再了解实现细节。
坚持编写测试,编写有效的测试,而不只是看测试覆盖率和测试数量,推荐使用TDD。
Alibaba COLA架构,就是为了提供一个可落地的业务代码结构规范,让代码腐烂的尽可能慢一些,让团队的开发效率尽可能快一些。
COLA 概述
架构的意义 就是 要素结构:
要素 是 组成架构的重要元素;
结构 是 要素之间的关系。
而 应用架构的意义 就在于
定义一套良好的结构;
治理应用复杂度,降低系统熵值;
从随心所欲的混乱状态,走向井井有条的有序状态。
COLA架构就是为此而生,其核心职责就是定义良好的应用结构,提供最佳应用架构的最佳实践。通过不断探索,我们发现良好的分层结构,良好的包结构定义,可以帮助我们治理混乱不堪的业务应用系统。
经过多次迭代,我们定义出了相对稳定、可靠的应用架构:COLA v4
COLA 架构
COLA的官方博文中是这么介绍的:
因为业务问题都有一定的共性。例如,典型的业务系统都需要:
接收request,响应response;
做业务逻辑处理,像校验参数,状态流转,业务计算等等;
和外部系统有联动,像数据库,微服务,搜索引擎等;
正是有这样的共性存在,才会有很多普适的架构思想出现,比如分层架构、六边形架构、洋葱圈架构、整洁架构(Clean Architecture)、DDD架构等等。
这些应用架构思想虽然很好,但我们很多同学还是“不讲Co德,明白了很多道理,可还是过不好这一生”。问题就在于缺乏实践和指导。COLA的意义就在于,他不仅是思想,还提供了可落地的实践。应该是为数不多的应用架构层面的开源软件。
COLA架构 区别于这些架构的地方,在于除了思想之外,我们还提供了可落地的工具和实践指导。
COLA分层架构
官方分层图:
以及官方介绍的各层的命名和含义:
注意:这里做了一些改动,在domain和infra层,新增了repository来访问数据库,不与gateway网关耦合在一起。
COLA组件
大部分组件比较简单,如果不满足企业诉求,建议进行简单的二次封装和改进。
一个简单的 Web Demo 🌰
Parent Pom
<modules>
<module>Eric-Cola-Demo-client</module>
<module>Eric-Cola-Demo-adapter</module>
<module>Eric-Cola-Demo-app</module>
<module>Eric-Cola-Demo-domain</module>
<module>Eric-Cola-Demo-infrastructure</module>
<module>start</module>
</modules>
Start 层
该模块作为整个应用的启动模块(通常是一个SpringBoot
应用),只承担启动项目和全局相关配置项的存放职责。
将启动独立出来,好处是清晰简洁,也能让新人一眼就看出如何运行项目,以及项目的一些基础依赖。
代码结构如下:
Adapter层
外部不同端的适配层,官方这样描述:
Controller这个名字主要是来自于MVC,因为是MVC,所以自带了Web应用的烙印。然而,随着mobile的兴起,现在很少有应用仅仅只支持Web端,通常的标配是Web,Mobile,WAP三端都要支持。
代码结构如下:
Client层
有了controller
层,接下来是不是应该到service
层了。
是,也不是。
传统的Web应用中,完全可以只有一个service
层给controller
层调用,但是作为一个业务应用,除非你真的只是个前端页面的无情吐数据机器,否则很大可能性你的应用会有很多其他上下游调用方,并且你需要提供接口给他们。
这时候你给他们的不应该是一个Web接口,应该是RPC调用的服务层接口。
所以在COLA中,你的adapter
层,调用了client
层,client
层中就是你服务接口的定义。
从上图中可以看到,client
包里有:
api文件夹:存放服务接口定义
dto文件夹:存放传输实体
注意,这里只是服务接口定义,而不是服务层的具体实现,所以在adapter
层中,调用的其实是client
层的接口 CustomerServiceI
:
@RestController
public class CustomerController {
private final CustomerServiceI customerService;
public CustomerController(CustomerServiceI customerService) {
this.customerService = customerService;
}
@GetMapping("/customer/page")
public PageResponse<CustomerDTO> pageCustomers(
@RequestParam(required = false, value = "customerId") String customerId,
@RequestParam(required = false, value = "companyName") String companyName
) {
CustomerListQuery customerListQry = new CustomerListQuery();
customerListQry.setCustomerId(customerId);
customerListQry.setCompanyName(companyName);
return customerService.pageCustomers(customerListQry);
}
}
而最终接口的具体实现逻辑放到了app
层,如下的CustomerServiceImpl
。
@Service("customerServiceImpl")
@CatchAndLog
public class CustomerServiceImpl implements CustomerServiceI {
@Resource
private CustomerCmdExecutor customerCmdExecutor;
@Resource
private CustomerQueryExecutor customerQueryExecutor;
public Response addCustomer(CustomerAddCmd customerAddCmd) {
return customerCmdExecutor.execute(customerAddCmd);
}
@Override
public MultiResponse<CustomerDTO> list(CustomerListQuery customerListQry) {
return customerQueryExecutor.listCustomers(customerListQry);
}
@Override
public PageResponse<CustomerDTO> pageCustomers(CustomerListQuery customerListQry) {
return customerQueryExecutor.doPageQuery(1, 10, () -> customerQueryExecutor.listCustomers(customerListQry));
}
}
app层
app模块作为服务的实现,存放了各个业务的实现类,并且严格按照业务分包,这里划重点,是先按照业务领域分包,再按照功能实现分包的。
customer和order分别对应了消费着和订单两个业务子领域。里面是COLA定义app层下面三种功能:
可以看到,消息队列的消费者和定时任务,这类平时我们业务开发经常会遇到的场景,也放在app层。同时做了些微调,加入了convertor。
Domain层
看一下整体结构:
可以看到,首先是按照不同的领域(customer
和order
)分包,里面则是4种主要的文件类型:
领域实体
实体模型可以是充血模型(自行了解),例如官方示例里的Customer.java
如下:
// Domain Entity can choose to extend the domain model which is used for DTO
@Data
public class Customer {
private String customerId;
private String memberId;
private String globalId;
private long registeredCapital;
private String companyName;
private SourceType sourceType;
private CompanyType companyType;
public Customer() {
}
public boolean isBigCompany() {
return registeredCapital > 10000000; //注册资金大于1000万的是大企业
}
public boolean isSME() {
return registeredCapital > 10000 && registeredCapital < 1000000; //注册资金大于10万小于100万的为中小企业
}
public void checkConflict() {
//Per different biz, the check policy could be different, if so, use ExtensionPoint
if ("ConflictCompanyName".equals(this.companyName)) {
throw new BizException(this.companyName + " has already existed, you can not add it");
}
}
}
领域能力
domainservice
文件夹下,是领域对外暴露的服务能力,如上图中的CreditChecker
领域网关
gateway
文件夹下的接口定义,这里的接口你可以粗略的理解成一种SPI,也就是交给infrastructure
层去实现的接口。
领域数据库访问
repository
文件夹下的接口定义,同`gateway
,也是交给infrastructure
层去实现的接口。
例如
CustomerRepository
里定义了接口queryByExample
,要求infrastructure
的实现类必须定义如何通过多请求参数获取Customer实体信息,而infrastructure
层可以实现任何数据源逻辑,比如,从MySQL获取,从Redis获取,还是从外部API获取等等。
public interface CustomerRepository {
List<Customer> queryByExample(String customerId, String companyName);
}
@Repository
public class CustomerRepositoryImpl implements CustomerRepository {
private final CustomerDOMapper customerMapper;
public CustomerRepositoryImpl(CustomerDOMapper customerMapper) {
this.customerMapper = customerMapper;
}
public List<Customer> queryByExample(String customerId, String companyName) {
CustomerDOExample customerDOExample = new CustomerDOExample();
if (StringUtils.isNoneBlank(customerId)) {
customerDOExample.or().andCustomerIdEqualTo(customerId);
}
if (StringUtils.isNoneBlank(companyName)) {
customerDOExample.or().andCompanyNameLike("%" + companyName + "%");
}
List<CustomerDO> customerDOList = customerMapper.selectByExample1(customerDOExample);
return customerDOList.stream().map(CustomerDOConvertor.CONVERTOR::toDomain).collect(Collectors.toList());
}
}
Infrastructure层
infrastructure也就是基础设施层,这层有我们刚才提到的repositoryimpl实现,也有MyBatis的mapper等数据源的映射和config配置文件。
最后,在引用一段官方介绍博客原文来总结COLA的层级:
适配层(Adapter Layer):负责对前端展示(web,wireless,wap)的路由和适配,对于传统B/S系统而言,adapter就相当于MVC中的controller;
应用层(Application Layer):主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的,应用层也可以绕过领域层,直接访问基础实施层;
领域层(Domain Layer):主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Domain Entity)的方法对App层提供业务实体和业务逻辑计算。领域是应用的核心,不依赖任何其他层次;
基础实施层(Infrastructure Layer):主要负责技术细节问题的处理,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。此外,领域防腐的重任也落在这里,外部依赖需要通过gateway的转义处理,才能被上面的App层和Domain层使用。
COLA架构的特点
领域与功能分包策略
也就是下面这张图的意思,先按照领域分包,再按照功能分包,这样做的其中一点好处是能将腐烂控制在该业务域内。
比如消费者customer
和订单order
两个领域是两个后端开发并行开发,两个人对于dto
,util
这些文件夹的命名习惯都不同,那么只会腐烂在各自的业务包下面,而不会将dto
,util
,config
等文件夹放在一起,极容易引发文件冲突。
业务域与外部依赖解耦
前面提到的domain
和infrastructure
层的依赖倒置,是一个非常有用的设计,进一步解耦了取数逻辑的实现。
例如下图中,你的领域实体是商品item
,通过gateway
接口,你的商品的数据源可以是数据库,也可以是外部的服务API。
如果是外部的商品服务,你经过API调用后,商品域吐出的是一个大而全的DTO(可能包含几十个字段),而在下单这个阶段,订单所需要的可能只是其中几个字段而已。你拿到了外部领域DTO,转为自己领域的Item
,只留下标题价格库存等必要的数据字段。
总结
COLA架构并不复杂,COLA已经从1.0版本经过逐次精简,发展到了如今的形态。通过脚手架能够快速生成多module的maven项目,节省很多项目初始化的时间。
没有银弹,如果不具备良好的OO设计和编程技能,那么即使使用各种落地架构进行指导,也只能写出越来越难以阅读与维护的代码。
最后,本文引用了官方和互联网博客的部分内容,如有侵权请告知删除。
参考
COLA 4.0:应用架构的最佳实践_cola4.0-CSDN博客
GitHub - alibaba/COLA: 🥤 COLA: Clean Object-oriented & Layered Architecture