阿里cola

cola是什么

cola是阿里大佬张建飞所提出的一种业务代码架构的最佳实践方案,并且已经在阿里云脚手架代码生成器中作为一个可选项,可见其有了一定影响力

COLA是Clean Object-Oriented and Layered Architecture的缩写,代表整洁面向对象分层架构

在cola4.0中,也就是目前的最新版本中,作者将cola拆分为cola架构(Archetype)和cola组件(components)俩个部分

  • cola架构:cola应用的代码模板
  • cola组件:提供一些非常有用的通用组件,这些组件可以帮助我们提升研发效率

俩这互不干扰,可以独立使用

架构

首先要探探cola架构,cola的官方博文中是这么介绍的

平时我们开发的大部分系统中都需要

  • 接收request,响应response
  • 做业务罗处理,像校验参数,状态流转,业务计算等
  • 和外部系统联动,像数据库,微服务,搜索引擎等

正是有这样的共性存在,才会有很多普适的架构思想出现,比如分层架构、六边形架构、洋葱圈架构、整洁架构(Clean Architecture)、DDD架构等等。
这些应用架构思想虽然很好,但我们很多同学还是“不讲Co德,明白了很多道理,可还是过不好这一生”。问题就在于缺乏实践和指导。COLA的意义就在于,他不仅是思想,还提供了可落地的实践。应该是为数不多的应用架构层面的开源软件。

Cola提供了一整套代码架构,拿来即用。其中包含了很多架构设计思想,包括讨论度很高的领域驱动DDD等

先来看俩张官方的介绍图

官方表格中,介绍了cola中每个层的命名和含义

这两张图和一个表格已经把整个COLA架构的绝大部分内容展现给了大家,但是一下子这么多信息量可能很难消化。
既然整个示例架构项目是一个Maven父子结构,那我们就从父模块一个个好好过一遍。
首先父模块的pom.xml包含了如下子模块:

<modules>
  <module>demo-web-client</module>
  <module>demo-web-adapter</module>
  <module>demo-web-app</module>
  <module>demo-web-domain</module>
  <module>demo-web-infrastructure</module>
  <module>start</module>
</modules>

分层

start层

该模块作为整个应用的启动模块通常是一个springboot应用,只承担启动项目和全局相关配置的存放职责

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

adapter层(适配器)

接下来我们按照之前架构图从上到下的顺序,一个个看。
首先是demo-web-adapter模块,这名字是不是很新鲜?但其实,可以理解为平时我们用的controller层(对于Web应用来说),换汤不换药。
在COLA官方博客中,也能找到如下的描述:

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

client(连接)

有了controoler层,接下来有小伙伴就肯定会想,是不是service层啦

是,也不是

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

这时候你给他们的不应该是一个web接口,应该是rpc调用的服务层接口,应该是RPC调用的服务层接口,至于原因不是本文的重点,具体就不展开了。

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

从上图中可以看到client包里有

  • api文件夹:存放服务接口定义
  • dto文件夹:存放传输实体

注意这里只是服务接口定义,而不是服务层的具体实现,所以在adapter层中,调用的其实是client层的接口

@RestController
public class CustomerController {

    @Autowired
    private CustomerServiceI customerService;

    @GetMapping(value = "/customer")
    public MultiResponse<CustomerDTO> listCustomerByName(@RequestParam(required = false) String name){
        CustomerListByNameQry customerListByNameQry = new CustomerListByNameQry();
        customerListByNameQry.setName(name);
        return customerService.listByName(customerListByNameQry);
    }

}

而接口的具体实现放到了app层

@Service
@CatchAndLog
public class CustomerServiceImpl implements CustomerServiceI {

    @Resource
    private CustomerListByNameQryExe customerListByNameQryExe;

    @Override
    public MultiResponse<CustomerDTO> listByName(CustomerListByNameQry customerListByNameQry) {
        return customerListByNameQryExe.execute(customerListByNameQry);
    }
}

app(应用)

接着上面说的,我们的app模块作为业务的实现,存放了各个业务的实现类,并且严格按照业务分包,这里划重点,是先按照业务分包,再按照功能分包的,为什么要这么做后面会说

customer和oder分别对应了消费者和订单俩个子业务领域。里面是cola定义app层面下面三种功能

可以看到消息队列的消费者和定时任务,这类平时我们业务开发经常会遇到的场景,也放在app层

domain(领域)

接下来是domain,也就是领域层,先看一下领域层整体结构

可以看到,首先是按照不同的领域(Customer和oder)分包,里面则是三种主要的文件类型

  1. 领域实体:实际模型可以是充血模型(之前讲过)官方示例
@Data
@Entity
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 checkConfilict(){
        //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");
        }

    }
}
  1. 领域能力:domainservice文件夹下是领域对外暴露服务的能力,如上图的CreditCheker
  2. 领域网关:getway文件夹下的接口定义,这里的接口可以粗略理解成一种SPI,也就是交给infrastructure层去实现的接口

例如CustomerGateWay里定义了getById,要求infrastructure的实现类必须定义如何通过消费者id获取消费者实体信息,而infrastructure层可以实现任何数据源逻辑,比如从mysql获取,从redis获取,还是从外部API获取等等

public interface CustomerGateway {
    public Customer getByById(String customerId);
}

在示例代码的CustomerGatewayImpl(位于infrastructure层)中,CustomerDo(数据库实体)经过mybatis的查询,转换了Customer领域实体,进行返回。完成了依赖倒置

@Component
public class CustomerGatewayImpl implements CustomerGateway {
    @Autowired
    private CustomerMapper customerMapper;

    public Customer getByById(String customerId){
      CustomerDO customerDO = customerMapper.getById(customerId);
      //Convert to Customer
      return null;
    }
}

infrastructure(基础实施层)

最后是我们的infrastructure也就是基础设施层,这层有我们刚才提到的gatewayImpl实现,也有Mybatis的mapper等数据源的映射的config配置文件

总结

适配层(Adapter Layer)负责对前端展示(web,wireless,wap)的路由和适配,对于传统B/S系统而言,adapter就相当于MVC中的controller

应用层(Application Layer)主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的吗,应用层也可以绕过领域层,直接访问基础实施层

领域层(Domain Layer)主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Domain entity)的方法对App层提供实体业务逻辑计算。领域是应用的核心,不依赖任何层次

基础实施层(Infrastructure Layer):主要负责及时细节问题处理,比如数据库的CURD,搜索引擎,文件系统,分布式服务的RPC等,此外领域的防腐重任也落在这里,外部需要通过getway的转义处理,才能被上面的app层和domain层使用

特色

也就是下面这张图的意思,先按照领域分包,再按照功能分包,这样做的其中一点好处是能将腐烂控制在该业务域内。

比如消费者customer和订单order两个领域是两个后端开发并行开发,两个人对于dto,util这些文件夹的命名习惯都不同,那么只会腐烂在各自的业务包下面,而不会将dto,util,config等文件夹放在一起,极容易引发文件冲突。

前面包的定义,都是给功能维度上的定义,为了兼顾领域维度的内聚性,我们有必要对包结构进行一下微调,即顶层包结构应该是按照领域划分,让领域内聚

业务域和外部依赖解耦
前面提到的domain和infrastructure层的依赖倒置,是一个非常有用的设计,进一步解耦了取数逻辑的实现。

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

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

Last modification:December 12, 2022
如果觉得我的文章对你有用,请随意赞赏