老代码多=过度耦合=if else?阿里巴巴工程师这样捋直老代码 | 技术头条
作者 | 紫思
本文经授权转自公众号“闲鱼技术”(ID: XYtech_Alibaba)
在业务开发的过程中,往往存在平台代码和业务代码耦合严重难以分离、业务和业务之间代码交织缺少拆解的现象。因此不论从代码质量,还是从团队协作的角度来看都严重地影响了开发团队之间的协同效率和开发效率,最终影响到了用户体验和业务发展。
在闲鱼,商品发布和编辑功能也是如此。本文将以闲鱼商品发布和编辑功能的改造为例,向大家展示闲鱼是如何解决此类问题。
发布编辑功能的升级改造
为了实现上述目标,针对发布和编辑功能,进行了两轮升级。第一轮的目标在于“平台和业务分离、业务和业务隔离”;而第二轮将更进一步,目标在于“系统之间的解耦合,提升团队协同效率”。
1、平台和业务分离,业务和业务隔离
第一轮改造中,闲鱼将原先的商品发布和编辑功能从老应用中抽取到了新应用item。为了实现“平台和业务分离、业务和业务隔离”的目标,闲鱼自研了一套技术框架SWAK,具体请参考文章《业务代码解构利器--SWAK》,该文介绍了其设计思想和实现原理。接入SWAK框架后,平台逻辑和业务逻辑得到了分离,各个业务(如租房业务、免费送业务)之间的逻辑也不再耦合,而是变成package隔离(当然也是可以做成jar包隔离)。
看一看改造后的应用情况示意图:
我们根据发布和编辑的主干流程,抽象了17个SWAK扩展点;
发布和编辑的主干流程主要就是对这些扩展点的编排。主干流程的编写并不需要考虑业务上怎么实现这些扩展点的;
我们根据不同的业务(在SWAK里面更准确的表述是tag)对这些扩展点进行了各自的实现。
根据这样的开发方式,我们可以把开发同学分成如下的两种角色:
各业务开发人员:各业务开发人员主要是负责各个业务相关的代码,在item应用里面,业务同学需要维护其业务中和发布编辑相关的个性化业务逻辑;
主干开发人员:主干的人员只需要维护主干的代码,尤其是扩展点的抽象,随着不同业务的不断接入,原先的扩展点也需要随之调整。
经过SWAK改造后,获得了如下的几个优点:
代码逻辑清晰,可变和不可变一目了然;
代码复用度变高;
可变逻辑按照标签进行隔离,单个标签的实现不会影响到其他标签的实现,降低开发和测试成本。无论是按照“类型”分还是按照类目分,对应的开发和测试同学只需要关注对应的逻辑即可;
新接手的开发人员能够快速理解,轻松上手。
2、系统之间的解耦合,提升团队协同效率
以租房为例——租房业务的同学需要在item应用中维护一套租房发布编辑相关的逻辑(如校验地小区数据、地铁数据真实性等);租房业务的同学还需要在详情应用的逻辑中维护一套和租房详情相关的逻辑(如展示地图,展示内部设施标签);租房业务的同学还需要在交易应用的逻辑中维护一套和租房交易相关的逻辑(如预约看房)等等。租房的同学不仅仅需要着手于自己的代码逻辑,还需要修改发布和编辑应用item、还需要修改详情应用,还需要修改交易应用......这种体验是非常糟糕的,有极大的可能性接手一个简单业务就需要修改和发布四五个应用。
另一方面,从主干开发人员的角度来说,其应用不仅仅由自己或自己的小团队来维护,还有很多业务开发人员也在修改和发布此应用,且频率会远远超过主干开发任务的发布和部署频次(否则就是主干扩展点逻辑抽取得不好了)。这不利于整个应用的稳定性。A业务服务挂了,应该只影响A业务,而不应该影响主干。依此逻辑,最好能做到JVM隔离。本质上来说,第一轮改造完成了业务之间的解耦合,而第二轮则是系统之间的解耦合。
“康威定律”告诉我们:
Any organization that designs a system (defined more broadly here than just information systems) will inevitably produce a design whose structure is a copy of the organization's communication structure.
简而言之就是人员组织结构和系统结构之间的一致性。而完成系统之间的解耦合又恰恰是符合康威定律的。这一轮的改造,我们称之为“业务服务化”。
业务服务化改造方案
首先,我们把租房业务给单独抽取出来。原先的帖子和拍卖业务暂时没有独立的团队来予以维护(但也基本上没有什么新需求)因此暂时仍然放在主干应用中,时机合适将会和租房应用一样迁移出去。
其次,租房业务通过远程服务的方式给主干应用提供服务。接口即是主干业务的提供的扩展点。由于现在是优先使用远程服务来连接主干应用和垂直应用,考虑到性能问题和安全问题,我们在扩展点的定义上也做了一些特殊的改动,后文会有针对性的详述。
最后,SWAK框架做了一些改变以注册和调用远程服务。相对于本地服务,远程服务一般都是有超时、连接异常等问题。然而不同接口对于这些异常情况其处理策略也是截然不同的,后文“SWAK框架的针对性改进”会详述这些改动。
通过这种方式,我们将主干应用和各业务应用彻底分离了。仍然以租房业务为例,租房团队负责开发和维护租房业务的独立应用rent。租房个性化的发布和编辑需求只需要开发和部署rent应用,而不必修改主干应用。主干应用只由主干团队的同学负责维护,不会被其他业务团队的同学所开发和部署,稳定性更加能得以保障。各业务系统独立开发、独立部署。这些都大幅地减少了不必要的沟通成本、提升协同效率。
主干应用和业务应用是通过薄薄的一层接口所联系起来的,这层薄薄的接口都是“声明”:Interface定义、DO的定义和扩展点的默认Reduce策略定义。
1、SWAK框架的针对性改进
在之前的《业务代码解构利器--SWAK》一文中指出了,SWAK框架在应用启动的时候会通过各种注册器(registery)注册框架所需的信息。其中最重要的信息就是——业务tag及其对应的SWAK接口的实现类类名或者类实例instance。大多RPC框架都会在client端提供一个代理,代理掉内部的服务发现、保活、序列化、网络通信、反序列化等一系列操作。
实际上,SWAK为了支持远程服务调用,只需要将业务tag,以及这些RPC的client的instance的对应关系注册进去就可以了。在闲鱼,RPC使用的是阿里通用的HSF框架(其类似的一个开源框架是Dubbo),这里的RPC的client就是HSF中的ConsumerBean。
上文还提到了RPC调用会引入服务超时、连接异常的概念。为何要限制超时?是因为不能被单个应用的超时占据了主干应用的服务资源而引起其他服务和整个应用系统受到影响(如大多数线程阻塞在超时调用上)。无论是超时异常还是连接异常,在业务上也有对应的处理策略。在这里,我们定义了三种异常处理策略,通过在配置上设置相应的注解,SWAK框架会自动按照策略来处理异常。这三种策略是:
IGNORE:即直接向上层抛出异常;
SKIP:对于一个接口有多个tag执行的时候,本tag下该扩展点将跳过,继续执行其他tag下该扩展点的实现;
DEFAULT_VALUE:返回默认值,默认值通过spel表达式进行设置。
2、减少扩展点数量
众所周知,RPC调用相对于本地调用会增加一部分的网络传输和序列化开销。对于单次调用来说,增加若干ms并没有什么问题,但对于调用10次、20次或更多,这笔开销就相当可观而应该引起重视了。
为此,如何降低RPC开销,是一个必须要考虑的问题。最可靠的方法就是降低RPC的次数。
在实践中我们发现,很多扩展点实际上都是获取业务配置。如在闲鱼业务中,“是否支持多库存”就是一种配置,如租房不支持多库存。这些业务配置项是由其业务形态所决定的,基本不会变动。因此可以将一组配置项打包一起调用,并且可以缓存下来,也可以直接由主干应用进行维护。在item应用里,这些配置项关系到主干的通用存储过程,目前由各业务方委托主干开发人员进行维护,目前配置在主干环境。可以通过阿里的动态配置平台(如Switch、Diamond)进行动态修改。
另外我们对部分邻接的扩展点进行了合并。这些相邻扩展点之间的逻辑比较简单,且不会中断主流程。通过“配置型接口”和“邻接扩展点合并”这两种操作,我们将扩展点数量降低由17个降低到了6个。要注意的是,扩展点并不是越少越好。扩展点越少,越意味着“过度拟合”,可能会对后续业务变更无法适应导致主干需要大幅改动,因此需要在数量和扩展性之间找到一个平衡。
另外值得一提的是,SWAK为配置型扩展点做了相应的小改造,并提供了查看当前配置型扩展点返回值的可视化界面。开发人员可以直观地了解当前各个业务的配置值。
3、接口对象定义和细节设计
在闲鱼,各种业务所需要存储的东西大同小异,从闲鱼的发布界面上来看就不难发现这一点,都是在基础对象(如标题、描述、图片)之外添加一些业务相关的数据,如拍卖业务中指定拍卖的开拍时间等信息,免费送业务中设置兑换币值,图书业务上设置条形码。即对一本图书进行拍卖当然也是允许的,这就出现了拍卖业务和图书业务叠加起来的复合型业务。
对于主干应用开发人员来说,应该提供单个接口以支持所有业务类型,这样不用每次修改或者新增业务时都需要提供新接口。从稳定性的角度考虑,这样的要求很合理。既然是单个接口,那么DO的定义也应该统一。以商品DO为例,有以下三种方式:
第一种是继承型结构,该结构不适用于业务叠加的情况。另外主干需要知晓各个业务的DO,每次业务修改或新增,主干都需要做变动。
第二种是组合型结构,适用于业务叠加的情况,但同上一种一样,主干需要知晓各个业务的DO,每次业务修改或新增,主干也需要随之变动。
第三种使用了Map类型类承载各个业务(biz)的定义类型。主干完全不知道、也不需要知道各个业务DO是如何组成的。这种方式具有最好的扩展性(有点无边界的扩展),也分离了主干应用和业务应用,最接近主干和业务分离的期望。最终我们选择了这一种。
使用第三种的对象模型,以新加一种业务为例,其开发流程是:
新业务服务端开发人员和客户端开发人员约定各业务的DO,这些DO会存储到bizMap字段,主干应用开发人员不需要了解这些约定;
主干应用新增一份新业务的配置,实际上是新业务的识别信息和路由信息;
新业务应用实现主干扩展点;
联调、测试和上线。
业务应用在扩展点返回值中设置需要更新的数据,由主干应用合并。业务应用不应该也不可以直接修改ItemDO,避免影响其他业务的处理逻辑。对于发布和编辑这种需要持久化存储的逻辑来说,必须要强控各业务对ItemDO的修改,否则理论上来说,各业务都有可能将所有的关键字段修改得面目全非。前面提到的“配置型接口”中,就有这样的配置——该业务是否可以修改属性字段、该业务是否可以修改描述字段等配置。
总结&规划
闲鱼的商品发布和编辑功能基于SWAK框架经过了两次改造升级,第一次升级完成了平台和业务之间的解耦合以及业务和业务之间的解耦合,第二次升级通过平台和业务间使用RPC调用完成了系统和系统之间的解耦合。改造之后,能更有效地协同更多团队更快更稳定地支撑各种业务。
SWAK框架依然在继续演进,如部分扩展点原则上可以通过并行处理或异步化处理来提升性能,但暂时还没有提供支持。在这两次改造中, 我们还在测试用例的采集、回放、监控告警等方面也有很多积累,敬请关注后续分享。
热 文 推 荐
print_r('点个好看吧!');
var_dump('点个好看吧!');
NSLog(@"点个好看吧!");
System.out.println("点个好看吧!");
console.log("点个好看吧!");
print("点个好看吧!");
printf("点个好看吧!");
cout << "点个好看吧!" << endl;
Console.WriteLine("点个好看吧!");
fmt.Println("点个好看吧!");
Response.Write("点个好看吧!");
alert("点个好看吧!")
echo "点个好看吧!"