作者:闲鱼技术-紫思
简介
业务的不断发展、商品类型的不断增多、不断添加的业务需求使得闲鱼的代码出现“bad smell”——平台代码和业务代码耦合严重难以分离;业务和业务之间代码交织缺少拆解。这也是行业中的通病。为解决此类问题,闲鱼自研了一套技术框架——SWAK。本文带大家一起看看SWAK是怎么解构闲鱼代码的。
SWAK是Swiss Army Knife的简称,众所周知,瑞士军刀是一款小巧灵活、适用于多种场景的工具。在闲鱼服务端,SWAK框架也是这样一种小巧灵活、适用于多种场景的技术框架, 它所要使用的场景都具有同一个特点——多实现间的规则化执行。本文将以一个例子开篇,来详细介绍其中的概念。
多实现和规则化执行
熟悉闲鱼的朋友们应该知道,在闲鱼App里面,商品有丰富的表现形式,不妨叫做类型A、类型B和类型C,各种类型也可以有各自的子类型。每种类型的业务逻辑存在一定的共性,但是也存在部分差异——如在分享页面中,subtitle字段的展示逻辑就不尽相同:
这种单一的实现通常会被写成如下的代码:
if(A类型) {
if(A1类型) {
doSomething1();
}else if(A2类型) {
doSomething2();
}
} else if(B类型) {
doSomething3();
} else if(C类型) {
if(C1类型) {
doSomething4();
}else if(C2类型) {
doSomething5();
}
}
类似的代码大家应该都写过不少。逻辑简单的时候写成这样无可厚非,但当逻辑开始变复杂的时候这种写法会具有较多的坏处:
- 难以抽出公共的逻辑,代码块愈发臃肿。
- 有较多相同点少量异同点的新类型的实现很难复用原先的代码。
- 各个类型的代码实际上融合在一块,更改代码可能会影响到其他类型,提高上线风险和测试回归成本。
- 对于新接手的开发人员来说,理解成本高,上手难度大,无形中降低开发效率。
按照面向对象的思想,获取title的方式对于所有类型都是一致的,应该沉淀成平台逻辑,而获取subtitle就可以抽象成一个接口方法,而类型A、类型B和类型C的宝贝都具有各自的实现而已。对于 获取subtitle这个接口方法来说,它有着多种实现。
那么什么是规则化执行呢?在上面的例子中,我们按照了商品的类型(type)进行了逻辑的分离,但通常情况下并非能分隔地如此彻底。举一个例子,运营团队的划分可能也按照商品类型(type)做划分,也有可能按照类目(category,如手机、3C数码、服饰、图书等)体系来做划分,甚至还有可能按照地域进行划分。那么一个商品可能既会受到商品类型体系的约束,又会受到类目体系的约束,还会受到地域的约束。如果几种约束不一致的话,就会产生冲突。比如subtitle字段,从类型A的视角上来看应该显示价格,在图书类目的视角下或许应该透出出版社——毕竟爱读书的人大多更关注质量而出版社是衡量质量的一个重要标准。是展示价格,还是出版社?或者都展示?如果都展示的话先展示价格还是先展示出版社?如果一行不够放下所有内容又怎么办?无论是上述的哪一种展示方式,背后都是“规则”(在设计模式里,称之为“策略”),代码也无非是按照“规则”进行编写而已。
以上的例子是多实现规则化执行的一个经典场景。类似地,如ABTest、双写等逻辑也是多实现规则化执行的应用场景。
基本思想
在上面的例子中,按照商品的类型或者按照商品的类目进行区分会产生冲突。其实无所谓类型或者类目,对于商品这个对象来说,无非是给其贴上了不同的标签而已——如一个类型A的图书类目宝贝被贴上“类型A”和“图书”两个标签。“类型A”的获取subtitle接口方法对应着一种实现,而“图书”的获取subtitle接口方法又对应着另一个实现。当一个对象被贴多个标签的时候,多个标签对应的实现就会产生冲突。
冲突的解决依赖于“规则”。“规则”最重要的两个部分是——优先级(Priority)和归约(Reduce)策略;执行的先后顺序由优先级决定,而显示第一个实现的结果、显示第二个实现的结果还是两个实现结果的拼接等都是归约策略。“规则”还可以包含如“并行执行方式”和“异常处理方式”等其他组成部分。
如上,可以得出SWAK的基本思想:
- 分析对象所具有的标签。
- 分离出不可变的逻辑和可变的逻辑。可变的逻辑抽象成接口。
- 可变的逻辑根据标签的不同有多种实现。每种实现是独立的,即每种实现是互相隔离的。
- 当对象同时具有多个标签时,使用优先级和归约策略来解决冲突问题。
值得一提的是,SWAK的基本思想借鉴自阿里巴巴中台的TMF架构,关于TMF的细节可以参考《尽在双11--阿里巴巴技术演进与超越》一书的《基于TMF框架的交易平台架构》章节。
相应地,使用SWAK框架将带来如下的好处:
- 代码逻辑清晰,可变和不可变一目了然。
- 代码复用度变高。
- 可变逻辑按照标签进行隔离,单个标签的实现不会影响到其他标签的实现,降低开发和测试成本。无论是按照“类型”分还是按照类目分,对应的开发和测试同学只需要关注对应的逻辑即可。
- 新接手的开发人员能够快速理解,轻松上手。
实现原理
相较于运行期才进行根据标签去扫描并加载实现类的方式,SWAK框架更倾向于在静态期就能分析出具有某几个标签的对象在不同的实现方法下会有着怎样的执行逻辑。一方面通过缓存可以明显降低响应时间,另一方面也便于在开发期间发现和排查问题。整体的实现原理可以分成两个部分:注册 和 执行。基本流程如下:
在注册过程中,SWAK框架将会扫描文件(多实现接口、归约策略、冲突优先级采用了Java注解或者XML文件进行了配置,下面的代码示例中介绍多实现接口和其实现类是如何配置的),扫描出的结果都注册到了本地缓存中,而在执行过程中SWAK框架会从本地缓存中直接查找其所需的冲突优先级配置和归约策略等,这样有助于减少响应时间。另外,使用统一的本地缓存有助于进行“可视化的展现”——开发人员可以直观地看到并分析出程序的执行流程;产品经理也可以直观地看到哪些功能点可以方便扩展,哪些地方的优先级需要更新等等,甚至有助于需求的估时和排期。使用统一的本地缓存也为“可视化的配置”提供了可能性,结合阿里内部的Diamond或者Switch框架(轻量级的开关和动态配置项管理框架),可以无需更新代码,仅需推送配置就可以更新冲突优先级,为开发和测试提供了极大的便利。
/**
* 此处用一个简单的demo演示下基本的配置,实际的业务要远比demo复杂
*/
@SwakInterface(desc="获取subtitle") // 使用注解声明这是一个多实现接口
public interface SubtitleFetcher {
@SwakMethod
String fetchSubtitle();
}
@SwakTag(tags = {"tagA"}) // 使用SwakTag绑定tagA的实现
@Component
public class TagASubtitleFetcher implements SubtitleFetcher {
@Override
public String fetchSubtitle() {
return "我是TagA";
}
}
@Component
@SwakTag(tags = {"tagB"}) // 使用SwakTag绑定tagB的实现
public class TagBSubtitleFetcher implements SubtitleFetcher {
@Override
public String fetchSubtitle() {
return "我是TagB";
}
}
闲鱼服务端应用基本都基于Spring框架。为了便于在服务端应用上使用SWAK框架,在设计之初,我们就要求SWAK需要100%地兼容Spring框架。最终的实现版本做到了这一点,无论是业务的bean还是SWAK框架自身引入的bean,都完全由Spring容器托管。框架还使用了cglib代理了上图里执行过程中的一系列流程,完全由框架执行,对开发同学是完全透明、无感知的,使用起来如普通的单实现的接口一般,如下代码块所示。
@Autowired
private SubtitleFetcher subtitleFetcher;
//省略大段代码.......
String subtitle = subtitleFetcher.fetchSubtile();
//省略大段代码.......
在闲鱼的应用情况
目前,SWAK框架在闲鱼已经在商品发布和编辑的部分流程上得以应用,我们正在积极将SWAK框架扩展到到更多的流程上。下图是基于SWAK框架的商品域核心功能的改造计划。经过基于SWAK的升级改造,闲鱼商品域核心功能按照业务隔离,各业务开发同学仅需关系其对应业务的开发即可,其通用逻辑和业务隔离由基于SWAK框架的一层和二层充分保证。代码质量和开发效率将获得显著提升。
总结
闲鱼自研的SWAK这一多实现规则化执行框架,可以很好地解决平台代码和业务代码耦合严重难以分离、业务和业务之间代码交织缺少拆解的问题。并且SWAK 100%兼容Spring,使用方便,快速上手。名副其实地,SWAK框架就像瑞士军刀一样可以适用于多种场景,小巧方便。当然,SWAK仍在不断进化,特性和功能仍在不断丰富。类似地,在闲鱼还有很多有意思的、创造性的尝试。如果对此感兴趣,欢迎加入我们。
简历投递:**guicai.gxy@alibaba-inc.com
**