如何保证同事的代码不会腐烂?一文带你了解 阿里巴巴 COLA 架构
一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
本文开始前,问大家一个问题,你觉得一份业务代码,尤其是互联网业务代码,都有哪些特点?
我能想到的有这几点:
- 互联网业务迭代快,工期紧,导致代码结构混乱,几乎没有代码注释和文档。
- 互联网人员变动频繁,很容易接手别人的老项目,新人根本没时间吃透代码结构,紧迫的工期又只能让屎山越堆越大。
- 多人一起开发,每个人的编码习惯不同,工具类代码各用个的,业务命名也经常冲突,影响效率。
- 大部分团队几乎没有时间做代码重构,任由代码腐烂。
每当我们新启动一个代码仓库,都是信心满满,结构整洁。但是时间越往后,代码就变得腐败不堪,技术债务越来越庞大。
这种情况有解决方案吗?也是有的:
- 小组内定期做代码重构,解决技术债务。
- 组内设计完善的应用架构,让代码的腐烂来得慢一些。(当然很难做到完全不腐烂)
- 设计尽量简单,让不同层级的开发都能快速看懂并上手开发,而不是在一堆复杂的没人看懂的代码上堆更多的屎山。
而COLA,我们今天的主角,就是为了提供一个可落地的业务代码结构规范,让你的代码腐烂的尽可能慢一些,让团队的开发效率尽可能快一些。
COLA是什么
COLA是由阿里大佬张建飞所提出的一种业务代码架构的最佳实践,并且已经在阿里云脚手架代码生成器中作为一个可选项,可见其已经拥有了一定影响力。
COLA 是 Clean Object-Oriented and Layered Architecture的缩写,代表“整洁面向对象分层架构”。
在COLA 4.0,也就是目前最新的版本中,作者将COLA拆分为COLA架构(Archetype)和COLA组件(Components)两个部分:
- COLA架构:COLA应用的代码模板。
- COLA组件:提供一些非常有用的通用组件,这些组件可以帮助我们提升研发效率。
两者互不干扰,可以独立使用。
COLA整体架构
首先主要谈谈COLA架构,COLA的官方博文中是这么介绍的:
在平时我们的业务开发中,大部分的系统都需要:
- 接收request,响应response;
- 做业务逻辑处理,像校验参数,状态流转,业务计算等等;
- 和外部系统有联动,像数据库,微服务,搜索引擎等;
正是有这样的共性存在,才会有很多普适的架构思想出现,比如分层架构、六边形架构、洋葱圈架构、整洁架构(Clean Architecture)、DDD架构等等。
这些应用架构思想虽然很好,但我们很多同学还是“不讲Co德,明白了很多道理,可还是过不好这一生”。问题就在于缺乏实践和指导。COLA的意义就在于,他不仅是思想,还提供了可落地的实践。应该是为数不多的应用架构层面的开源软件。
COLA提供了一整套代码架构,拿来即用。 其中包含了很多架构设计思想,包括讨论度很高的领域驱动设计DDD等。
注意:每个人对于架构设计都有着自己的理解。所以对于COLA的架构,本篇文章也仅仅只是我自己对于COLA的粗浅理解,大家可以批判看待。
COLA分层架构
先来看两张官方介绍图
其次,还有一个官方的表格,介绍了COLA中每个层的命名和含义:
层次 | 包名 | 功能 | 必选 |
---|---|---|---|
Adapter层 | web | 处理页面请求的Controller | 否 |
Adapter层 | wireless | 处理无线端的适配 | 否 |
Adapter层 | wap | 处理wap端的适配 | 否 |
App层 | executor | 处理request,包括command和query | 是 |
App层 | consumer | 处理外部message | 否 |
App层 | scheduler | 处理定时任务 | 否 |
Domain层 | model | 领域模型 | 否 |
Domain层 | ability | 领域能力,包括DomainService | 否 |
Domain层 | gateway | 领域网关,解耦利器 | 是 |
Infra层 | gatewayimpl | 网关实现 | 是 |
Infra层 | mapper | ibatis数据库映射 | 否 |
Infra层 | config | 配置信息 | 否 |
Client SDK | api | 服务对外透出的API | 是 |
Client SDK | dto | 服务对外的DTO | 是 |
这两张图和一个表格已经把整个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三端都要支持。
cilent层
有了我们说的“controller”层,接下来有的小伙伴肯定就会想,是不是service层啦。
是,也不是。
传统的Web应用中,完全可以只有一个service层给controller层调用,但是作为一个业务应用,除非你真的只是个前端页面的无情吐数据机器,否则很大可能性你的应用会有很多其他上下游调用方,并且你需要提供接口给他们。
这时候你给他们的不应该是一个Web接口,应该是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和order分别对应了消费着和订单两个业务子领域。里面是COLA定义app层下面三种功能:
App层 | executor | 处理request,包括command和query | 是 |
---|---|---|---|
App层 | consumer | 处理外部message | 否 |
App层 | scheduler | 处理定时任务 | 否 |
可以看到,消息队列的消费者和定时任务,这类平时我们业务开发经常会遇到的场景,也放在app层。
domain层
接下来便是domain,也就是领域层,先看一下领域层整体结构:
可以看到,首先是按照不同的领域(customer和order)分包,里面则是三种主要的文件类型:
- 领域实体:实体模型可以是充血模型(请自行了解),例如官方示例里的Customer.java如下:
@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");
}
}
}
领域能力:domainservice文件夹下,是领域对外暴露的服务能力,如上图中的CreditChecker
领域网关:gateway文件夹下的接口定义,这里的接口你可以粗略的理解成一种SPI,也就是交给infrastructure层去实现的接口。
例如CustomerGateway里定义了接口getByById,要求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配置文件。
Infra层 | gatewayimpl | 网关实现 | 是 |
---|---|---|---|
Infra层 | mapper | ibatis数据库映射 | 否 |
Infra层 | config | 配置信息 | 否 |
所有层讲完了,COLA4.0很简单明了,最后,在引用一段官方介绍博客原文来总结COLA的层级:
1)适配层(Adapter Layer):负责对前端展示(web,wireless,wap)的路由和适配,对于传统B/S系统而言,adapter就相当于MVC中的controller;
2)应用层(Application Layer):主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的,应用层也可以绕过领域层,直接访问基础实施层;
3)领域层(Domain Layer):主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(Domain Entity)的方法对App层提供业务实体和业务逻辑计算。领域是应用的核心,不依赖任何其他层次;
4)基础实施层(Infrastructure Layer):主要负责技术细节问题的处理,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。此外,领域防腐的重任也落在这里,外部依赖需要通过gateway的转义处理,才能被上面的App层和Domain层使用。
COLA架构的特色
说完了分层架构,我们再来回顾下上面提到的COLA架构的几个特色的设计
领域与功能的分包策略
也就是下面这张图的意思,先按照领域分包,再按照功能分包,这样做的其中一点好处是能将腐烂控制在该业务域内。
比如消费者customer和订单order两个领域是两个后端开发并行开发,两个人对于dto,util这些文件夹的命名习惯都不同,那么只会腐烂在各自的业务包下面,而不会将dto,util,config等文件夹放在一起,极容易引发文件冲突。
前面的包定义,都是功能维度的定义。为了兼顾领域维度的内聚性,我们有必要对包结构进行一下微调,即顶层包结构应该是按照领域划分,让领域内聚。
业务域和外部依赖解耦
前面提到的domain和infrastructure层的依赖倒置,是一个非常有用的设计,进一步解耦了取数逻辑的实现。
例如下图中,你的领域实体是商品item,通过gateway接口,你的商品的数据源可以是数据库,也可以是外部的服务API。
如果是外部的商品服务,你经过API调用后,商品域吐出的是一个大而全的DTO(可能包含几十个字段),而在下单这个阶段,订单所需要的可能只是其中几个字段而已。你拿到了外部领域DTO,转为自己领域的Item,只留下标题价格库存等必要的数据字段。
COLA并不完美
诚然,COLA已经做的足够清晰简洁了,但是它仍然有不完美的地方,比如每个接口的出入参都会根据业务名做定义,导致了很多结构极为相似的DTO,DTO的爆炸增长是个问题。参考:ISSUE-271
但是总的来说,COLA只是给你提供了一种架构设计的思想,并不深入到强制你使用某种规范的层面,所以对于COLA中你觉得复杂,或者不理解的地方,很多时候需要你自己来做权衡,作取舍。取其精华,去其糟粕的运用到你的项目中。
总结
COLA架构并不复杂,COLA已经从1.0版本经过逐次精简,发展到了如今的形态。在阿里云代码脚手架生成器中作为一个可选项,足见其已经趋于成熟。
下一篇文章,我会和大家一起讨论下COLA组件库中的一些重要组件,比如扩展点组件(cola-component-extension-starter),状态机组件(cola-component-statemachine)等。这些组件并不强制和COLA绑定,你完全可以不使用这些组件,只使用COLA架构来设计你的应用。但是这些组件可以提升团队的研发效率。
我们下期再见,我是在阿里搬砖的工程师 蛮三刀酱
持续的更新原创优质文章,离不开你的点赞,转发和分享!
我的唯一技术公众号:后端技术漫谈
欢迎加我个人号:BrodyYang
参考
COLA Github
https://github.com/alibaba/COLA
COLA 4.0.0 版本
https://blog.csdn.net/significantfrank/article/details/110934799
COLA 3.1.0 版本
https://blog.csdn.net/significantfrank/article/details/109529311
COLA 3.0.0 版本
https://blog.csdn.net/significantfrank/article/details/106976804
COLA 2.0.0 版本
https://blog.csdn.net/significantfrank/article/details/100074716
COLA 1.0.0 版本
https://blog.csdn.net/significantfrank/article/details/85785565
如何保证同事的代码不会腐烂?一文带你了解 阿里巴巴 COLA 架构的更多相关文章
- 还看不懂同事的代码?超强的 Stream 流操作姿势还不学习一下
Java 8 新特性系列文章索引. Jdk14都要出了,还不能使用 Optional优雅的处理空指针? Jdk14 都要出了,Jdk8 的时间处理姿势还不了解一下? 还看不懂同事的代码?Lambda ...
- 代码中,使用__DATE__宏,获取程序编译时间,如何保证每次编译代码(非重新生成方式),都能更新__DATE__的值?
代码中,使用__DATE__宏,获取程序编译时间,如何保证每次编译代码(非重新生成方式),都能更新__DATE__的值? 解决:通过vs的预先生成命令中,添加批处理命令,删除对应的obj文件方式,强制 ...
- 我的第一个python web开发框架(23)——代码版本控制管理与接口文档
书接上一回,小白和老菜聊到代码的版本控制和接口文档 小白:为什么要做版本控制,我不弄版本控制不也完成了项目了吗?要做版本控制不是很麻烦,又要安装服务又要提交代码,代码又不是多人用开发,还要写文档... ...
- 试试使用 eolinker 扫描 GitLab 代码注释自动生成 API 文档?
前言: 一般写完代码之后,还要将各类参数注解写入API文档,方便后续进行对接和测试,这个过程通常都很麻烦,如果有工具可以读取代码注释直接生成API文档的话,那会十分方便. 此前一直都是在使用eolin ...
- 代码详解:TensorFlow Core带你探索深度神经网络“黑匣子”
来源商业新知网,原标题:代码详解:TensorFlow Core带你探索深度神经网络“黑匣子” 想学TensorFlow?先从低阶API开始吧~某种程度而言,它能够帮助我们更好地理解Tensorflo ...
- 百度架构师带你进阶高级JAVA架构,让你快速从代码开发者成长为系统架构者
百度架构师带你进阶高级JAVA架构,让你快速从代码开发者成长为系统架构者 1.
- Typora代码块配色和标题自带序号的实现代码
Typora代码块配色和标题自带序号的实现代码 先打开主题文件夹 文件>偏好设置>外观>打开主题文件夹 然后编辑base.user.css(如果没有就新建一个)文件 /*标题自动添加 ...
- 《java入门第一季》之HashSet存储元素保证唯一性的代码及图解
上一篇介绍了HashSet存储自定义自定义对象时应该注意的问题http://blog.csdn.net/qq_32059827/article/details/51580642, 这一篇对其内部结构稍 ...
- 还看不懂同事的代码?Lambda 表达式、函数接口了解一下
当前时间:2019年 11月 11日,距离 JDK 14 发布时间(2020年3月17日)还有多少天? // 距离JDK 14 发布还有多少天? LocalDate jdk14 = LocalDate ...
随机推荐
- 『德不孤』Pytest框架 — 11、Pytest中Fixture装饰器(一)
目录 1.Fixture装饰器的用途 2.Fixture参数说明 3.Fixture装饰器简单应用 4.yield执行后置函数 1.Fixture装饰器的用途 做测试前后的初始化设置,如测试数据准备, ...
- cve-2018-12613-PhpMyadmin后台文件包含漏洞
前言 刚开始复现这个漏洞的时候是在自己的本机上然后跟着大佬的复现步骤可是没有预期的结果最后看了另一篇文章 当时整个人都麻了 首先何为phpMyAdmin 根据官方的说明phpMy ...
- c++ set与unordered set的区别
c++ std中set与unordered_set区别和map与unordered_map区别类似,其底层的数据结构说明如下: 1.set基于红黑树实现,红黑树具有自动排序的功能,因此map内部所有的 ...
- 跨平台跨架构的统信DTK开发套件教程及常见问题
DTK是统信基于Qt开发的一整套简单且实用的通用开发框架,处于统信UOS系统中的核心位置,统信UOS浏览器.音乐.邮件等40余款原生应用全部使用DTK开发.DTK从开发者的角度出发,融合现代化的开发理 ...
- ArcGIS Server 禁用/rest/services路径(禁用服务目录)
ArcGIS Server服务目录(路径如:http://<hostname>:6080/arcgis/rest/services)默认可以不需要登陆直接打开.效果如下图. ArcGIS服 ...
- 【网鼎杯2020白虎组】Web WriteUp [picdown]
picdown 抓包发现存在文件包含漏洞: 在main.py下面暴露的flask的源代码 from flask import Flask, Response, render_template, req ...
- Gradle Gretty进行runAppDebug的Listening for transport dt_socket at address: 5005 的后续配置
出现 Listening for transport dt_socket at address: 5005:代表debug端口已启动好了,接下来你需要进行配置远程Debug,进行附加Debug进程: ...
- NO Oracle database,JUST USE Oracle client。远程导入导出dmp
序言: 你会发现,exp.exe 和imp.exe均存在于Oracle数据库的安装bin目录下.而很多情况下,我们不想安装庞大的Oracle数据库,但想使用imp和exp等工具命令,在我们本地机对Or ...
- Java中自动装箱与拆箱
一.什么是封装类? Java中存在基础数据类型,但是在某些情况下,我们要对基础数据类型进行对象的操作,例如,集合中只能存在对象,而不能存在基础数据类型,于是便出现了包装器类.包装器类型就是对基本数据类 ...
- Redis 的回收策略(淘汰策略)?
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最 少使用的数据淘汰 volatile-ttl:从已设置过期时间的数据集(server.db[i ...