前言
天猫优品导购归因链路负责天猫优品订单导购判定工作,目前支撑了天猫优品权益券导购、普通导购和淘花导购等多种导购类型。随着业务迭代,现有导购归因链路在维护性、扩展性和可读性等方面存在明显不足,代码复杂性不断攀升,历史代码债务逐步积累。
为解决上述问题,开展了天猫优品导购归因链路技术重构工作。进一步地,在导购归因重构基础上,作为对“属性-分类-执行”问题的产品化思考与实践,提出了一种通用归因技术组件 ACE 。ACE 组件基于属性校验器、分类器和执行器三层模型解决了属性分类的通用性问题,具有良好的扩展性和代码语义。
本文以天猫优品导购归因重构为背景,阐述了一种基于 ACE 组件的订单归类技术方案。
技术痛点
伴随业务快速上下线,现有天猫优品导购归因链路在不断迭代过程中逐渐积累历史代码债务,应用代码存在复杂性高、扩展性低、可读性差等问题。
▐ 事务脚本编程
事务脚本编程导致代码复杂性攀升。事务脚本型代码可能我们每天都在写,我们有时在用一门面向对象语言写着面向过程代码,基于 IF-ELSE 等条件判断语句快速堆砌业务代码。每新增一行业务代码,也许就新增了一行代码债务,应用代码复杂性逐步攀升。
图 1 :代码复杂性的演变
▐ 违背开闭原则
违背开闭原则导致可维护性差。每次业务需求迭代,都在原有业务代码基础上修改或新增逻辑。我们很难知道历史代码哪些地方埋了坑,最好的方式就是尽量避免改动它。实际上,对于大部分业务代码,很难保证在新增需求时完全不需要改动原有代码逻辑。
▐ 缺失架构设计
缺失架构设计导致可扩展性差。业务型技术团队常常面临业务需求急、开发周期短等问题,为支撑新业务快速上线,有时会采取最快的方式满足业务诉求。然而,最快的方式往往缺失架构设计,只为满足单一需求,对后续迭代并不友好。随着源源不断的新需求,应用代码很快陷入破窗效应,可扩展性越来越差,代码债务不断积累。
▐ 业务逻辑复杂
复杂业务逻辑导致代码可读性差。看到下面这段代码,可能很难理解满足哪些属性是导购订单。这样的代码在业务型技术团队很常见,我们带着业务需求打开应用代码,却发现连原有代码所表示的业务含义都难以理解。代码可读性对业务型技术团队尤为重要,因为代码往往隐藏着业务含义,复杂的业务场景加上晦涩难懂的应用代码无疑是雪上加霜。
图 2 :晦涩难懂的业务代码逻辑
重构目标
为解决上述技术痛点,结合天猫优品导购业务发展背景,制定以下重构目标。
▐ 精简导购归因链路,清理过时业务逻辑
业务发展存在不断试错的过程,应用代码伴随着业务不断迭代。有些业务代码虽然早已过时,且由于团队开发人员流动,谁也不敢轻易删除历史代码。代码上线容易下线难,代码愈发臃肿。因此,有必要精简现有天猫优品导购归因逻辑,清理过时业务逻辑。
▐ 抽象业务模型,向后兼容业务发展
结合现有业务场景,抽象业务模型,支持后续业务轻量化迭代。通过抽象业务模型,可降低应用代码复杂度与业务场景复杂度的强相关性,甚至实现同一模型支撑多种不同业务场景。
▐ 完善业务优先级决策,规则统一收口
规范业务优先级决策,统一收口业务优先级规则,便于后续代码维护和业务迭代。优先级决策是一种很常见的业务规则。如何用一行代码描述所有业务优先级,而不是将业务优先级判断散落在应用的多处地方?
▐ 提升代码可读性,代码语义即业务语义
借助通用业务模型,赋予代码更丰富的语义,提升代码可读性。代码可读性对业务型技术团队尤其重要,看懂代码即看懂业务规则,可极大减少沟通成本,提升开发效率。
技术方案
基于现有技术痛点和重构目标,首先抽象业务模型,然后设计了一种通用归因技术组件 ACE,最后将 ACE 应用于天猫优品导购归因链路。
▐ 业务模型
以导购归因为例,导购归因旨在判断一个订单存在哪种类型的有效导购行为。有效性定义可概括为两个方面:一是满足或过滤某些属性,二是满足业务优先级规则。
导购归因是订单归类和优先级决策的组合,具体概括为以下四个步骤:
Step 1 :导购订单必须满足或不满足某些属性
例如,天猫优品导购订单必须满足天猫优品商品等属性,且不满足(过滤)本地履约订单等属性。
Step 2 :不同属性组合成不同类型导购订单
例如,权益券导购订单 = 天猫优品商品订单 + 权益券订单 + ... + 非本地履约订单 + 非定向优惠订单。
Step 3 :不同导购订单类型存在不同业务优先级
根据优先级规则决策哪种类型导购订单有效。例如,权益券导购订单优先级高于普通导购订单。
Step 4 :根据归因结果执行不同处理流程
例如,订单判定为导购订单,执行落库、打标、消息推送等流程。
进一步地,导购归因可抽象为“属性-分类-执行”问题,抽象模型如下:
属性校验器(Attributor):表示一种属性。校验是否满足某个属性,支持原子或组合属性。
分类器(Classifier):表示一种类型。绑定一个或多个属性校验器,校验是否满足某些属性组合。分类器可分为嵌套分类器(NestedClassifier)和原子分类器(AtomicClassifier)。例如,Classifier 1 需要满足多个 Attributor,Classifier 4 需要满足 Classifier 1 和 Classifier 2。
执行器(Executor):表示一种类型对应的执行策略。绑定一个分类器,负责对某种类型执行处理。
图 3 :模型层次结构 Attributor-Classifier-Executor
▐ 归因组件
基于现有业务场景,抽象了一种“属性-分类-执行”的技术模型。在通用模型基础上,设计了一种归因组件 ACE 。ACE 是 Attributor - Classifier - Executor 的缩写,旨在通过属性校验器(Attributor)、分类器(Classifier)和执行器(Executor)三层模型解决属性分类的通用性问题。
整体设计
ACE 对外暴露统一服务接口 AceWorker,AceWorker 接收外部传入参数(归因场景 + 归因对象),根据归因场景获取分类器,并判断归因对象是否满足该分类器。分类器是 ACE 的核心,绑定了一个或多个属性校验器,并对应唯一的执行器。
图 4 :ACE 整体设计
详细设计
ACE 组件由 ACE 注解、ACE 工厂容器、ACE 初始化和 ACE 服务入口组成,详细设计如图 5 所示。
ACE 注解
基于易用性考虑,ACE 提供三种注解 @Attributor、@Classifier 和 @Executor 用于声明 ACE 组件,分别对应属性校验器、分类器和执行器。
- @Attributor:声明一个属性校验器,属性校验器名称唯一。
- @Classifier:声明一个分类器,分类器名称唯一。@Classifier 提供 matcher、filter 和 priority 三种属性,matcher 用于指定该分类器需满足的属性校验器列表,filter 用于指定该分类器需过滤的属性校验器列表,priority 用于指定该分类器绑定的原子分类器的优先级规则。
- @Executor:声明一个执行器,每个分类器对应一个执行器,执行器名称需与分类器名称一致。
ACE 工厂容器
AceFactory 是 ACE 的工厂容器,负责管理所有定义的 ACE 组件,包括属性校验器集合、分类器集合及其绑定的属性校验器集合、执行器集合。根据 ACE 组件名称可直接从 AceFactory 获取对应的 ACE 组件。
ACE 初始化
借助 AceInitService 初始化 ACE 组件,应用启动时 AceInitService 自动解析 ACE 注解,并将 ACE 组件注册到 ACE 工厂容器。
ACE 服务入口
AceWorker 是 ACE 的服务入口,负责对外提供 ACE 通用服务,如属性校验 attribute、分类 classify 和执行 execute 。
图 5 :ACE 详细设计
示例
1)定义属性校验器
定义属性校验器 A ,判断是否满足属性 A 。
/**
* 属性校验器示例 ATTRIBUTOR_A
* @author haoyu.chy
* @date 2020/9/5
*/
@Attributor(name = "ATTRIBUTOR_A")
public class AttributorA implements IAttributor {
@Override
public AceResult attribute(AceContext aceContext) {
if (满足属性A) {
return new AceResult(true);
}
return new AceResult(false);
}
}
2)定义分类器
定义原子分类器 CLASSIFIER_X(绑定属性校验器 ATTRIBUTOR_A ),判定是否满足类型 X 。
/**
* 原子分类器示例
* matcher:需匹配的属性
* filter:需过滤的属性
* @author haoyu.chy
* @date 2020/9/5
*/
@Classifier(name = "CLASSIFIER_X", matcher = "ATTRIBUTOR_A", filter = "")
public class ClassifierX implements AtomicClassifier {
}
定义嵌套分类器 CLASSIFIER_NEST ,绑定原子分类器 CLASSIFIER_X 和 CLASSIFIER_Y ,CLASSIFIER_X 优先级高于 CLASSIFIER_Y 。
/**
* 嵌套分类器示例
* priority:表示绑定的原子分类器的优先级规则
* @author haoyu.chy
* @date 2020/9/5
*/
@Classifier(name = "CLASSIFIER_NEST", priority = "CLASSIFIER_X,CLASSIFIER_Y")
public class Classifier_Nest implements NestedClassifier {
}
3)定义执行器
定义原子分类器 CLASSIFIER_X 对应的执行器 ExecutorX 。
/**
* 执行器示例
* 注:执行器名称对应分类器名称
* @author haoyu.chy
* @date 2020/9/5
*/
@Executor(name = "CLASSIFIER_X")
public class ExecutorX implements IExecutor {
@Override
public AceResult execute(AceContext aceContext) {
// do something ...
return new AceResult();
}
}
4)定义服务入口
定义归因服务入口,指定归因场景 aceScene 和归因对象 aceObject,借助 AceWorker 完成归因工作。
/**
* 归因服务入口示例
* @author haoyu.chy
* @date 2020/9/5
*/
public class SimpleService {
public static void main(String[] args) {
// 归因场景(例如,CLASSIFIER_NEST)
String aceScene = "CLASSIFIER_NEST";
// 被归因对象(例如,订单号)
Long aceObject = 1234567890L;
// 初试化上下文
AceContext<Long> aceContext = AceContext.of(aceScene, aceObject);
// 执行归因流程
AceResult aceResult = AceWorker.getInstance().classify(aceContext);
}
}
▐ 天猫优品导购归因
天猫优品导购订单类型举例:
- 权益券导购订单:订单存在权益券使用记录
- 天猫优品普通导购订单:天猫优品商品且最近一次导购记录为普通导购
- 天猫优品淘花导购订单:天猫优品商品且最近一次导购记录
为淘花导购
不同类型订单有不同优先级,业务规则如下:
- 优先级规则:权益券导购优先级最高,普通导购和淘花导购优先级并列(最近原则)
- 互斥规则:天猫优品定向优惠订单、本地履约订单、代购订单优先级高于所有类型导购订单
基于 ACE 组件,重构天猫优品导购归因技术链路,设计天猫优品导购归因流程如图 6 。
图 6 :天猫优品导购归因流程
Step 1 :定义属性校验器
属性校验器相互独立,表示是否满足某种属性。例如,定义属性校验器(Attributor):天猫优品商品标、权益导购券、普通导购记录、淘花导购记录、定向优惠、本地履约等。
Step 2:定义原子分类器
原子分类器绑定多个属性校验器,表示是否满足某种类型。例如,定义分类器(Classifier):权益券导购订单(COUPON_GUIDE)、普通导购订单(NORMAL_GUIDE)和淘花导购订单(SUPERB_GUIDE)。其中,权益券导购订单(COUPON_GUIDE)绑定权益导购券属性,过滤定向优惠和本地履约等属性。
注:图 6 实线表示满足,虚线表示过滤
Step 3:定义嵌套分类器
嵌套分类器绑定多个原子分类器,一般用于优先级决策场景,按前后顺序匹配第一个有效分类器。例如,定义嵌套分类器(GUIDE_ORDER),其绑定原子分类器(COUPON_GUIDE、NORMAL_GUIDE 和 SUPERB_GUIDE),优先级从前往后。
Step 4:定义执行器
每个原子分类器对应一个执行器。如果某个分类器有效,则执行对应的执行器。
Step 5:定义服务入口
定义 ACE 组件后,只需指定归因场景(对应分类器名称)和归因对象,即可使用归因服务。例如,归因场景为导购归因(GUIDE_ORDER),归因对象为某个订单,则表示对某个订单执行导购归因。
基于 ACE 组件,定义属性校验器、分类器和执行器,封装服务接口。天猫优品导购归因代码框架如图 7 。
图 7 :天猫优品导购归因代码框架
效果分析
▐ 遵循 SOLID 原则
单一责任原则(SRP):每个 ACE 组件只负责一种职责。Attributor 只判断是否满足某种属性,Classifier 只判断是否满足某种类型,Executor 只对某种类型执行处理。
开放关闭原则(OCP):ACE 组件之间相互独立。新增属性或分类无需修改原有组件,只需定义一种新的属性校验器或分类器即可,完全无需改动原有代码。
里氏替换原则(LSP):父类可用子类替代,子类只扩展父类方法,不重写父类方法。ACE 基于接口实现,不重写已实现方法。
接口隔离原则(ISP):子类不被迫依赖它不需要的方法。ACE 组件接口相互独立,且只提供唯一方法。
依赖倒置原则(DIP):ACE 组件面向接口编程,基于 ACE 工厂容器实现依赖注入,组件间不存在直接依赖关系。
举例:
如图 8 所示,新增导购类型(优盟导购),只需新增分类器 Classifier(UM_GUIDE),并绑定相关属性 Attributor(优盟),关联执行器 Executor(优盟导购)。借助 ACE 组件可无侵入性地新增业务类型,完全无需改动原有代码。
图 8 :新增导购类型(优盟导购)
▐ 代码结构优化
代码结构从原有的「纵向+多出口」转变成「横向+单出口」。从代码维护角度,单出口程序更利于维护,代码(方法)出口统一收口到一处地方,代码逻辑一目了然。
图 9 :横向单出口的代码结构
总结
本文提出了一种通用归因技术组件 ACE,并将其应用于天猫优品导购归因技术链路。ACE 组件以属性校验器、分类器和执行器三层模型为核心,规范化定义某种类型需满足的属性组合及其对应的执行策略。
ACE 组件借鉴了策略模式思想,不同分类器对应不同执行器(策略),但只有分类器有效时,相应执行器(策略)才会被执行。分类器支持组装式关联多个属性校验器,同一属性校验器可被多个分类器复用,具有较好的灵活性和扩展性。此外,ACE 组件可将代码逻辑结构化,提升应用代码的可读性。
思考
一周岁技术新人的非严谨思考
“业务需求的局部性原理”
大家都听过计算机系统的局部性原理,其实业务需求也存在“局部性原理”。小到多打一行日志、多传一个参数,大到多留一个扩展点、抽象一个服务,这都在为后续需求留余地。写代码时多做一步,不写一次性代码,也许反而能减少后续工作量。
“应用代码的破窗效应”
在实际需求开发过程中,我们往往会参考原有代码实现。代码风格或结构设计是具有传染性的,糟糕的代码风格和架构设计会使应用陷入破窗效应。一个不成熟的思考,是否绝大部分应用都难逃破窗效应?即使前期有较好的架构设计,但由于业务发展和人员流动,原有架构约束依然有可能过时或被忽略。
“业务团队的技术挑战”
在集团的“关怀”下,业务型技术团队的技术越做越轻,业务越做越重。开箱即用的中间件,让业务团队变得“没有技术挑战”。刚参加工作的这一年,常常焦虑个人技术成长。回过头想,对于业务团队而言,技术挑战也许在广不在深。大多数情况下,技术型团队或许在和机器打交道,考验技术深度与钻研能力;业务型团队则在和商业打交道,考验技术架构与抽象能力。孰好孰坏似乎没有绝对答案。
淘系技术部-天猫优品团队
阿里集团新零售战略板块的重要一环,围绕消费电子/家装等领域,以天猫优品数字化门店为核心,沉淀一套从供给端到消费端的全链路数字化解决方案。
如您在寻求新的工作机会,欢迎投递简历至:haoyu.chy@alibaba-inc.com ,期待您的加入!
关注「淘系技术」微信公众号,一个有温度有内容的技术社区~