文章

Alibaba COLA 4.0 架构实践

Alibaba COLA架构 4.0

COLA 是 Clean Object-Oriented and Layered Architecture的缩写,代表“整洁面向对象分层架构”。 目前COLA已经发展到COLA v4

互联网业务项目一般会遇到如下一些普遍问题:

  • 虽然整体架构规划做的不错,但落地严重偏离,缺乏足够的抽象和弹性设计,面向流程编程。

  • 业务的工期紧、迭代快,导致代码结构混乱,几乎没有代码注释和文档,即使有项目代码规范。

  • 人员变动频繁,接手别人的老项目,新人根本没时间吃透代码结构,也很难快速了解上下文,紧迫的工期又只能让屎山越堆越大。

  • 多人协作开发,每个人的编码习惯不同,工具类代码各用个的,业务命名也经常冲突,团队成员庞大后更加影响效率。

  • 看似相同的功能,却很难加入改动,却经常听到:要写这张卡,先把之前的哪哪改了。

  • Code Review效果不佳,很难快速了解别人的上下文,只能简单看到一些命名、设计原则或明显的实现问题。

  • 大部分团队几乎没有时间做代码重构,任由代码腐烂。或者没有动力或KPI进行代码重构。

  • 不写单元测试,或编写的大量单元测试用处不大,有新功能加入或重构时导致要修改大量的测试。

每当新启动一个代码仓库,都是信心满满,结构整洁。但是时间越往后,代码就变得腐败不堪,技术债务越来越庞大…

有无好的解决方案呢?也是有的:

  1. 设计完善的应用架构以及代码落地规范,定期进行Review,让代码的腐烂来得慢一些。(当然很难做到完全不腐烂)

  2. 定期做代码重构,解决技术债务

  3. 设计尽量保持简单,让不同层级的开发都能快速看懂并上手开发,而不是在一堆复杂的没人看懂的代码上堆更多的屎山。

  4. 设计尽量遵守SOLID原则,开发人员经常违反单一职责原则和开闭原则,导致代码和测试调整困难。

  5. 坚持Code Review,先看模型设计,再了解实现细节。

  6. 坚持编写测试,编写有效的测试,而不只是看测试覆盖率和测试数量,推荐使用TDD

Alibaba COLA架构,就是为了提供一个可落地的业务代码结构规范,让代码腐烂的尽可能慢一些,让团队的开发效率尽可能快一些。

COLA 概述

架构意义 就是 要素结构:

  • 要素 是 组成架构的重要元素;

  • 结构 是 要素之间的关系。

应用架构意义 就在于

  • 定义一套良好的结构;

  • 治理应用复杂度,降低系统熵值;

  • 从随心所欲的混乱状态,走向井井有条的有序状态。

arch why

COLA架构就是为此而生,其核心职责就是定义良好的应用结构,提供最佳应用架构的最佳实践。通过不断探索,我们发现良好的分层结构,良好的包结构定义,可以帮助我们治理混乱不堪的业务应用系统。

cure

经过多次迭代,我们定义出了相对稳定、可靠的应用架构:COLA v4

COLA 架构

COLA的官方博文中是这么介绍的:

因为业务问题都有一定的共性。例如,典型的业务系统都需要:

  • 接收request,响应response;

  • 做业务逻辑处理,像校验参数,状态流转,业务计算等等;

  • 和外部系统有联动,像数据库,微服务,搜索引擎等;

正是有这样的共性存在,才会有很多普适的架构思想出现,比如分层架构、六边形架构、洋葱圈架构、整洁架构(Clean Architecture)、DDD架构等等。

这些应用架构思想虽然很好,但我们很多同学还是“不讲Co德,明白了很多道理,可还是过不好这一生”。问题就在于缺乏实践和指导。COLA的意义就在于,他不仅是思想,还提供了可落地的实践。应该是为数不多的应用架构层面的开源软件。

COLA架构 区别于这些架构的地方,在于除了思想之外,我们还提供了可落地的工具和实践指导。

COLA分层架构

官方分层图:

image-20240313113150809

image-20240313113217301

以及官方介绍的各层的命名和含义:

层次

包名

功能

必选

Adapter层

web

处理页面请求的Controller

Adapter层

wireless

处理无线端的适配

Adapter层

wap

处理wap端的适配

App层

executor

处理request,包括command和query

App层

consumer

处理外部message

App层

scheduler

处理定时任务

Domain层

model

领域模型

Domain层

service

领域能力,包括DomainService

Domain层

gateway

领域网关,解耦利器

Domain层

repository

领域数据访问

Infra层

gatewayimpl

网关实现

Infra层

repositoryimpl

数据库访问实现

Infra层

mapper

ibatis数据库映射

Infra层

config

配置信息

Client SDK

api

服务对外透出的API

Client SDK

dto

服务对外的DTO

注意:这里做了一些改动,在domain和infra层,新增了repository来访问数据库,不与gateway网关耦合在一起。

COLA组件

组件名称

功能

依赖

cola-component-dto

定义了DTO格式,包括分页

cola-component-exception

定义了异常格式, 主要有BizExceptionSysException

cola-component-statemachine

状态机组件

cola-component-domain-starter

Spring托管的领域实体组件

cola-component-catchlog-starter

异常处理和日志组件

exceptiondto组件

cola-component-extension-starter

扩展点组件

cola-component-test-container

测试容器组件

大部分组件比较简单,如果不满足企业诉求,建议进行简单的二次封装和改进。

一个简单的 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应用),只承担启动项目和全局相关配置项的存放职责。

将启动独立出来,好处是清晰简洁,也能让新人一眼就看出如何运行项目,以及项目的一些基础依赖。

代码结构如下:

image-20240313114206378

Adapter层

外部不同端的适配层,官方这样描述:

Controller这个名字主要是来自于MVC,因为是MVC,所以自带了Web应用的烙印。然而,随着mobile的兴起,现在很少有应用仅仅只支持Web端,通常的标配是Web,Mobile,WAP三端都要支持。

代码结构如下:

image-20240313114416233

Client层

有了controller层,接下来是不是应该到service层了。

是,也不是。

传统的Web应用中,完全可以只有一个service层给controller层调用,但是作为一个业务应用,除非你真的只是个前端页面的无情吐数据机器,否则很大可能性你的应用会有很多其他上下游调用方,并且你需要提供接口给他们。

这时候你给他们的不应该是一个Web接口,应该是RPC调用的服务层接口。

所以在COLA中,你的adapter层,调用了client层,client层中就是你服务接口的定义。

image-20240313114758728

从上图中可以看到,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模块作为服务的实现,存放了各个业务的实现类,并且严格按照业务分包,这里划重点,是先按照业务领域分包,再按照功能实现分包的

image-20240313115210066

customer和order分别对应了消费着和订单两个业务子领域。里面是COLA定义app层下面三种功能:

App层

executor

处理request,包括command和query

App层

consumer

处理外部message

App层

scheduler

处理定时任务

App层

convertor

处理数据对象之间的转换

可以看到,消息队列的消费者和定时任务,这类平时我们业务开发经常会遇到的场景,也放在app层。同时做了些微调,加入了convertor。

Domain层

看一下整体结构:

image-20240313115429139

可以看到,首先是按照不同的领域(customerorder)分包,里面则是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层去实现的接口。

image.png

领域数据库访问

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配置文件。

Infra层

gatewayimpl

网关实现

Infra层

repositoryimpl

数据库访问实现

Infra层

dataobject

数据对象定义

Infra层

convertor

数据对象DO的转换

Infra层

mapper

ibatis数据库映射

Infra层

config

配置信息

image-20240313121020776

最后,在引用一段官方介绍博客原文来总结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两个领域是两个后端开发并行开发,两个人对于dtoutil这些文件夹的命名习惯都不同,那么只会腐烂在各自的业务包下面,而不会将dto,util,config等文件夹放在一起,极容易引发文件冲突。

image.png

业务域与外部依赖解耦

前面提到的domaininfrastructure层的依赖倒置,是一个非常有用的设计,进一步解耦了取数逻辑的实现。

例如下图中,你的领域实体是商品item,通过gateway接口,你的商品的数据源可以是数据库,也可以是外部的服务API。

如果是外部的商品服务,你经过API调用后,商品域吐出的是一个大而全的DTO(可能包含几十个字段),而在下单这个阶段,订单所需要的可能只是其中几个字段而已。你拿到了外部领域DTO,转为自己领域的Item,只留下标题价格库存等必要的数据字段。

item2

总结

COLA架构并不复杂,COLA已经从1.0版本经过逐次精简,发展到了如今的形态。通过脚手架能够快速生成多module的maven项目,节省很多项目初始化的时间。

没有银弹,如果不具备良好的OO设计和编程技能,那么即使使用各种落地架构进行指导,也只能写出越来越难以阅读与维护的代码。

最后,本文引用了官方和互联网博客的部分内容,如有侵权请告知删除。

参考

COLA 4.0:应用架构的最佳实践_cola4.0-CSDN博客

GitHub - alibaba/COLA: 🥤 COLA: Clean Object-oriented & Layered Architecture

License:  CC BY 4.0