循序渐进搭建复杂B端系统整洁架构
作者:京东零售 赵嘉铎
前言:信息时代技术更迭和传播速度不断加快,技术变得泛娱乐化,大数据、云计算、区块链、元宇宙、大模型,一代代技术热点在社会舆论的裹挟之下不断地吸引着资本的眼球,技术人员为了不被时代所淘汰也不得不时刻追赶潮流。在这样一个时代背景下,软件工程作为一门不起眼到有些枯燥的古老学科,似乎早已被开发者们遗忘在角落。作为一名技术人员我们自然应该时刻保持对前沿技术的追踪,然而,当发生线上问题我们却面对着成片的屎山代码毫无头绪时;当业务方提出个性化需求我们却因为不敢对系统做出修改而强迫对方做出妥协时;当一次请求处理流程中出现多达数万次重复地数据库操作而影响到整个系统的稳定性时,大家都应该沉下心来思考一下,我们是不是忘记了作为一名程序员的初心和对代码的极致追求。 还记的当年我抱着朝圣的心态从传统行业踏入京东职场时的兴奋与期待,然而这份期待很快就被四处可见的屎山代码给浇灭了,后来从朋友口中了解到其他头部互联网厂商的业务系统其实也是半斤八两。这似乎是软件行业中的一个电车难题,一边是无尽的业务需求和倒排的工期,一边是补丁摞补丁的糟糕代码,是继续泡在酱缸中缝缝补补还是向屎山代码说不,开发人员被困在中间不知该如何抉择。然而事实上,追求整洁架构与提升研发效率之间从来就不是一个悖论。正如Robert C.Martin在其著作《Clean Architecture》中所说:“不管你多敬业、加多少班,(在面对烂系统时)你仍然会寸步难行,因为你大部分的精力是在应对混乱(而不是在开发需求)。”造成我们整日加班赶需求和疲于应对线上问题的根本原因,恰是那些不被我们重视的糟糕代码。业务天然就是复杂的,这决定了软件系统的本质复杂度(Essential Complexity),这种复杂度是无法通过软件架构去消除的。那么解决上述问题的关键就是找到某种架构去引导开发者对复杂业务进行问题拆解,分而治之,在这个基础上再通过标准规约和工具约束及辅助开发者写出可理解、易拓展、好维护的代码,以此来对抗软件系统本身的偶然复杂度(Accidental Complexity,Frederick P.Brooks,Jr, 《The Mythical Man-Month》)。 为了找到这样的一种架构,我们从19年就开始对各类架构思想和实践案例进行了深入地学习和探索,并在接下来的3年时间里通过局部架构演进的方式进行了大量的实践验证,在这个过程中我们对这些架构思想的理解也从早期的懵懂教条式执行逐渐做到了如今的融汇贯通,并最终在22年底形成了一套成体系的框架及方法论,并在京东广告投放平台重构工作中进行了实战应用。本文也将以广告投放平台架构升级作为背景案例,从设计思想到落地框架,循序渐进地为您介绍这套新架构的诞生始末,而这套架构思想的演进历程则在《改进我们的架构》一文中有详细的阐述。
一、架构升级背景
与高并发请求给C端系统带来的系统高性能、高可用能力挑战相比,B端系统所面临的挑战则是如何在海量多维度、多模块、多场景融合的复杂业务需求中保持系统健康、稳定、快速地迭代。京东广告投放平台就是一个典型的复杂B端业务系统,它承担着集成广告业务体系中各个垂直业务模块,构建、维护和分发广告物料的重要职责。经过多年迭代,京东广告投放系统目前已集成40余个垂直业务系统,支撑7条核心产品线,先后赋能10余个独立投放平台,维护着一个拥有200余个业务实体的庞大数据模型,每天都需要处理海量长事务、多系统交互的复杂业务请求。同时作为整个广告业务链路上的首发环节和功能门面,广告投放系统每年都要承接400余个来自不同业务方的差异化需求、执行1000余次代码合并及600余次功能发布。在极高的需求密度之下,作为撬动广告主预算的重要战场,投放系统在为广告主提供优秀投放体验的同时,还需要每天向广告业务链路稳定输送PB级的物料数据,这对系统的性能、稳定性以及团队的研发效能提出了极高的要求。

广告投放平台是一个典型的多平台、多模块集成的复杂B端系统
二、传统架构的研发痛点
近年来随着技术和业务的飞速发展,新的广告业务形态和投放组件层出不穷,广告物料结构愈发复杂。与此同时,为了提高广告主留存和撬动预算,业界各大平台都在向着极简版、智能化和集成化的方向发展。这些新的业态发展方向一方面给广告主带来了更加便捷和流畅的投放体验,另一方面也让投放系统内部业务流程愈发复杂,如何用有限的研发人力快速支撑越来越多的多场景复杂业务需求成为各大广告投放平台必须要解决的关键问题。然而传统的“三层架构+面向数据库编程”的研发模式由于过于简单的封装及粗暴的设计思想在面对这些高复杂度业务需求时变得愈发吃力,逐渐成为阻塞研发效能提升的罪魁祸首。
客观:传统架构面对高复杂度的业务时毫无应对之法
作为一个典型的Web应用,广告投放系统长期以来采用的都是传统的三层架构,这种没有架构的架构极其简单、易上手,因此一直以来都是业界的主流。但是由于它缺少统一明确的逻辑拆分与封装工具,业务的复杂度会等比渗透到代码实现中,进而导致系统的代码复杂度飙升,模块之间随意耦合,逻辑纠结缠绕,经过几轮迭代之后就成了看不懂、动不了、不敢动的酱缸代码。这些看似基础的编码问题实际上却是阻碍我们研发效能提升的罪魁祸首:
主观:“面向数据库编程”的设计思维让系统加速腐化
我们的业务本质就是获取、处理、存储及传输数据,在传统架构中业务逻辑通常以事务脚本(Transaction Script)的形式实现:业务规则直接在开发者的大脑中转化为数据库的增删改查操作(这也是很多程序员调侃自己是CRUD工程师的原因),然后被写到代码里。这种模式在场景单一、需求简单的业务发展早期阶段可以快速实现功能,但是随着业务复杂度的提升,这种过于粗糙的设计思维所带来的问题就会逐渐显现出来:
要想解决上述问题,就亟需一种面向未来的架构思想来指导我们对系统进行全面地升级。在此背景下,业界众多平台纷纷进行了领域驱动设计思想的探索和尝试,经典的案例有阿里的星环与COLA、快手的Baldr等,京东也推出了藏经阁平台与Matrix框架。这些实践案例和架构迭代路线给了我们很多启发,本着脚踏实地、事实就是的基本原则,在经过充分调研和长期验证之后,我们立足于京东广告业务的本质特征推出了一套可复用的复杂B端业务支撑框架,其核心内容可以分为PICASO能力编排框架与聚合及资源库机制两部分。
网络上能够找到很多介绍领域驱动设计思想的文章,但是大多都聚焦在对领域驱动设计中众多术语和概念的介绍上,对领域驱动设计思想的落地实践却浅尝辄止。再加上中英文语境的差异和国内外软件开发生态的不同,都在很大程度上将领域驱动设计思想“妖魔化”了,让很多同学望而却步或者不得其要义。然而我们在摸索实践的过程中逐渐意识到,领域驱动设计作为一种软件架构设计的指导思想其实并没有创造什么新的东西,而是对基本的软件设计思想进行的系统化总结和升华。但正是这种系统性的归纳将各类技巧、准则和思想凝练成体系化的方法论,并且在行业内形成了被所有开发者所公认的行为准则,这才是领域驱动设计思想强大生产力的源泉和魅力所在。 与传统的三层架构相比领域驱动设计思想其实并没有复杂多少,其要义就在于保持业务、模型与代码三者的统一,只要掌握了这一点,领域驱动设计思想中的各种理念都将是水到渠成的事情。初读《领域驱动设计》时书中众多晦涩的术语也曾让我十分困惑,但其中的很多内容其实已经是很多优秀架构师的工作日常了。随着对领域驱动设计思想理解的逐渐深入,我不时会产生“咳,这说的不就是xxx么”的感慨。这也是没有办法事,谁让国外那些提前入局的大佬们牢牢掌握着专业领域的命名权呢。也正因为如此,在本文中我们不会去介绍、甚至会尽量避免引用领域驱动设计理论中的术语,避免大家一开始就陷入到那些晦涩难懂的概念里而无法自拔。希望大家能将更多的精力放在框架内各个模块的设计动机与运行机制上,这才是我们最应该思考和关注的内容。至于领域驱动设计思想在新架构演进过程中的指导作用我们将会在《领域驱动设计与PICASO框架》一文中进行详细地介绍。
三、升级措施
(一)PICASO框架:从混乱到有序,构建图书馆式的代码架构
图灵奖得主Frederick在其著作《The Mythical Man-Month》中将软件系统的复杂度划分为本质复杂度(Essential Complexity)和偶然复杂度(Accidental Complexity),其中本质复杂度是问题本身所具有的复杂度,与求解方法无关,而偶然复杂度是求解方法引入的复杂度。本质复杂度无法避免,但是我们可以通过优化求解方法来尽可能降低系统的偶然复杂度。这给了我们很大的启发,业务天然就是复杂的,这是一个客观事实,架构设计的目标不是消除业务上的本质复杂度,而是应该引导和辅助开发者更好的拆解和分析业务带来的复杂度(是handle而不是eliminate)。同时,软件架构应该提供足够灵活的标准规约与框架工具,让所有开发者都能够按照统一的思想写出可理解、易拓展和好维护的代码,减少甚至是消除由于没有封装或封装不统一带来的偶然复杂度。在这一思想的指导之下,经过两年多的打磨,我们推出了PICASO框架。
PICASO概述
PICASO是一套以领域驱动设计(Domain-Driven Design, DDD)作为思想内核,专门为集成式复杂业务系统设计的通用基础框架。它的命名来自“PICASO Is a Contextual Ability Separate and Orchestrate Framework(PICASO是一种基于上下文的能力分解与编排框架)”的首字母缩写。有趣的是这个缩略词的发音恰好与西班牙现代派绘画大师毕加索(Picasso)的姓名读音相同,毕加索在画作中经常对人体部位进行解构和重组,在接下来的介绍中我们将发现这一点与PICASO框架所强调的能力拆分与编排思想有异曲同工之妙,而这也是我们最终采纳这个命名的原因。
PICASO的命名启发自笔者比较喜爱的一个开源项目——WINE,其功能是通过内核适配器在Linux环境中运行Windows应用程序,其命名也是这种藏头诗的风格:WINE Is Not Eumlator(WINE不是模拟器)。
PICASO框架的职责是引导开发者将复杂业务流程正交分解为多个简单子问题,然后将这些简单子问题的处理逻辑封装为边界明确的标准可执行实体,在PICASO框架中这些可执行实体被称为领域能力。完成能力拆解之后,开发者可以通过PICASO提供的能力编排框架将不同的领域能力的组合成一个完整的请求处理流程,这个处理流程所在的可执行实体就是一个领域服务。领域服务会为每次请求生成一个上下文对象,通过这个上下文对象可以在不同领域能力以及领域能力与领域服务之间进行数据传递与共享,进而避免重复及碎片化的IO操作。PICASO框架还提供了开箱即用的通用可执行实体发现与路由组件,开发者可以通过该组件按功能域对领域能力及领域服务进行分组和聚合,每个分组对外暴露统一的请求路由门面,从而向上层调用实体屏蔽分组内部的场景复杂度,进而实现复杂度降维。如果在领域能力或领域服务的路由维度之外还存在其他维度的细微逻辑差异,开发者可以通过PICASO提供的拓展点机制进一步实现差异点分离。同样的,拓展点依然可以接入通用可执行实体发现及路由组件,向上层实体屏蔽拓展点所在功能域内的场景复杂度。
上文对PICASO框架的整体架构进行了整体地介绍,接下来我们将从软件系统复杂度根源分析开始,循序渐进地详细阐述PICASO各个模块的设计动机及运行机制。

PICASO框架整体架构
复杂度的根源
软件设计的本质就是持续对抗软件本身产生的复杂度,早在最开始进行新架构探索的时候我们就意识到,构建整洁架构的前提是厘清系统复杂度的根源。
本质复杂度
通过对复杂业务系统发展历程的分析,我们发现业务复杂度一般来自水平方向上的多维度拓展和垂直方向上的多模块集成。
业务发展的早期往往都是单一场景,随着业务的发展,产品形态开始变得丰富多样,服务的用户及业务方也越来越多,业务架构从原来的单点结构逐渐演变为复杂的树状结构,树的每一层都代表一个业务维度,业务的发展让系统在水平方向上呈现出多维度增长的特征。以广告投放系统为例,最初的投放系统只有合约展示包段一种业务形态,随着程序化广告和智能广告的兴起,广告投放及播放形式层出不穷,业务树中开始出现“产品线”的维度;而为了服务不同业务方,我们在系统中增加了“投放平台”的维度;对不同投放标的物的支持又在系统中引入了“计划类型”的维度......就这样广告投放系统的业务架构也逐渐演变成了如下图所示的复杂树状结构。

多维度、多模块、多场景的广告投放业务
而在垂直方向上,早期的业务流程一般比较简短,只有少数几个业务环节。随着业务的发展,系统功能越来越丰富,业务流程也变得愈发冗长,开始呈现出鲜明的模块化特征。同样以广告投放系统为例,早期的广告物料只有时段、预算、出价、创意几个基础模块,随着业务的发展陆续新增了智能出价、人群定向、地域定向、商品定向、智能创意、智能选品等业务模块,物料创编流程也越来越冗长。除此之外,单个模块内部也开始出现多场景分化,如广告投放系统中的智能出价模块内部就存在tCPA、tROI、eCPC、MC等不同的智能出价模型,其数据模型及业务规则也不尽相同,这进一步增加了业务的复杂度。
本小节从业务架构演进历程的视角分析了业务复杂度的来源,这构成了系统的本质复杂度。而对这些复杂业务规则的实现方案(好的、或者是坏的)就成了系统偶然复杂度的来源。
偶然复杂度
业务在多个维度上向着熵增的方向不断发展,但是我们的代码始终只有一套,不同维度的业务场景可能对同一个业务环节提出不同的个性化需求,造成不同维度的业务逻辑互相耦合,代码中开始出现大量层层嵌套的if-else分支,圈复杂度不断飙升,系统开始出现腐化迹象。此时一些工程师可能会意识到这个问题并开始着手优化,但是由于缺少统一的逻辑封装与拓展工具,再加上开发者的水平与技法也不尽相同,导致优化方案五花八门,这种方案上的不一致反而进一步增加了代码的复杂度。除此之外,随着系统集成的业务模块越来越多,业务流程愈发冗长,与外部子系统的交互逻辑越来越复杂,开发者不得不去处理超时、重试、幂等、长事务、分布式事务及跨系统的数据一致性等问题,这些技术方案的引入对系统来说也是复杂度的来源。
对架构设计的启发
从上面的论述中可以看出,系统偶然复杂度的高低在很大程度上取决于开发者能否分析处理好业务的本质复杂度,另外在多人协作开发场景中,软件架构的标准性和解决方案的一致性也是决定系统偶然复杂度的重要因素,这就是我们推出PICASO框架的根本原因。我们希望PICASO能够引导开发者对复杂业务流程进行模块化拆解,采用分治思想逐一击破,并通过标准的逻辑封装规约与框架来实现多维度逻辑拓展,让团队中每一位开发者都能够以统一的思想写出清晰、简洁、有序、可检索的代码。
到这里相信有些读者可能会产生一些疑问,既然软件系统的偶然复杂度是技术方案本身的复杂度,那么引入PICASO框架是否也在增加系统的偶然复杂度呢?答案是肯定的,新框架的引入的确会增加系统的偶然复杂度。PICASO框架由于采用了全新的设计思想,在推行早期曾经历过痛苦的磨合期,也出现过不少由于开发者不理解新架构的运行机制而导致的设计缺陷或线上问题。但是任何架构迭代之路都是螺旋上升的,新技术带来的系统复杂度毕竟是静态的,随着开发人员对新架构运行机制及使用技巧的逐渐掌握,系统便开始趋于稳定,新技术带来的优化收益也会逐渐显现出来。但是如果我们不对现有的架构做出升级,那么系统将随着源源不断的业务需求向着不可控熵增的方向不断发展,由此带来的系统复杂度将是动态且持续增加的。
PICASO的复杂度应对之道
在分析完系统的复杂度来源之后,接下来我们将详细介绍PICASO如何协助开发者对抗软件系统的复杂度。IEEE对软件架构的定义为:架构是由系统之间的组织、组件及组件之间的关系、以及对设计与演进的指导原则组成的,其中前两者是具体的实体框架,后者是指导思想。而软件架构的指导思想往往决定着前两者的实现,对指导思想的理解与掌握程度也直接决定了开发者能否在实际业务中用好架构。以Spring框架为例,Spring的指导思想为:控制反转(IoC)、依赖注入(DI)及面向切面编程(AOP),这三大核心思想一方面直接决定了Spring框架核心模块的实现,另一方面也是开发者要想用好Spring则必须掌握的内容。而对PICASO来说,其指导思想可以概括为:能力拆分、拓展点抽象及能力编排。

软件架构的构成
领域能力拆分与路由助力多模块集成
神经认知学家乔治·米勒在他的论文《神奇的数字7》中指出人脑能够同时处理的信息容量是有限的,人脑的短时记忆容量为7(7个数字、6个字母或5个单词),后来的研究更是将这个数字降到了4个左右。所以当冗长的业务流程叠加上多维度的个性化诉求,系统的业务复杂度将飙升为
分离
关注点分离(Separation of concerns,SOC)就是把复杂问题正交分解为多个互不相关的最小子问题,聚焦整体问题的局部复杂性,逐步进行求解。我们在《复杂度的根源》章节中指出,复杂的业务系统往往会呈现出鲜明的模块化特征,因此我们可以自然而然地根据业务模块的功能边界对冗长的业务流程进行拆分,然后聚焦单个模块进行设计与抽象,避免陷入多模块、多场景互相耦合的思维泥沼。PICASO框架为此引入了领域能力及领域服务的概念,其中领域能力用来承接单个业务模块内部的逻辑细节,而领域服务则负责通过组合不同的领域能力实现一个完整的业务流程。如下图所示,以单元新建流程为例,我们可以把单元新建流程划分为:单元基础信息构造、优化目标设置、出价设置、人群设置、地域定向设置和商品定向设置多个子模块,我们可以将这些模块内部逻辑封装成领域能力,然后通过这些能力的组合构建一个完整的单元信息领域服务。

一个完整的业务流程可以拆分为多个原子业务模块,每个原子业务模块还可以按照其内部的业务模式进行进一步细分
PICASO框架中的领域服务与DDD思想中的领域服务是同一个概念,其职责和定位都是承接无法在单个实体与值对象内部直接实现的业务逻辑(事实上,B端系统对外提供的大部分服务都无法在单个聚合内直接实现)。而领域能力的概念则经常出现在一些企业级中台化框架中,如阿里的星环、京东的Matrix等。尽管当年如火如荼的中台化战略如今已经偃旗息鼓,但是我们还是将这个命名引入到了PICASO中,因为我们确实没有找到一个比它更合适的命名,可以如此形象地描述一个足够内聚、自治且能够被复用和拓展的原子实体。当然PICASO中的领域能力与那些企业级中台化框架中的领域能力相比要轻量和易用的多,不需要繁琐的身份申请,也不存在跨工程热加载的问题,毕竟中台化的重心在管理域平台及前中台团队的协作上,而PICASO则始终聚焦在代码本身的复杂度控制上。其实中台化也好,组件化也罢,系统的复杂度就摆在那里,不管用什么由头,要想提升团队整体的研发效能,它都是我们必须要去解决的一个问题。 在本小节的论述中,领域能力似乎就是根据业务模块的边界简单划分出来的。但是在实际开发中的能力划分要复杂的多,需要综合考虑能力的应用场景、会被哪些领域服务使用、以及能力之间的依赖关系等诸多因素进行反复地推导和调整。本文对能力划分方法论只是简单地做了问题引入,更加具体的内容我们将在《PICASO框架最佳实践——能力识别与划分》一文中进行详细介绍。
领域能力的拆解除了能够降低业务流程分析的复杂度之外,也提高了代码复用和拓展的灵活性。领域能力就像积木一样,可以被组装到不同的领域服务中,如人群设置能力可以同时被单元新建服务、单元编辑服务、人群快捷修改等领域服务复用。而能力拆解带来的拓展灵活性性是相对于朴素模版设计模式而言的。在传统架构中模板类可能是我们使用最多的设计模式,它的确能够简单有效地实现复用共性流程、分离差异的目标。但是由于复杂业务流程中不同业务节点的差异化维度往往是不同的,直接将业务主流程抽象成一个模板类,将各个节点作为模板中的抽象方法,那么该模板类子类的继承关系复杂度将是各个业务节点内部场景复杂度的叉乘。再加上传统架构并没有积极引导开发者落实面向对象编程的思想,导致我们基本上还在以面向过程的方式开发我们的系统,通常会将同一个产品线中不同的业务方法实现到同一个Service或者Manager类中,这将进一步加重模板抽象及子类继承关系的复杂度。而PICASO框架通过领域能力拆解将不同的业务环节拆分到了单独的原子业务实体中,将模板中的抽象方法算子化。由于不同的原子业务模块之间互相正交、互不干扰,因此能够让这些业务算子独立迭代,在各自的业务维度上灵活地进行继承和拓展。
分类
只是把业务流程按照功能边界拆分成不同的模块通常是不够的,因为单个模块内部往往还存在细分的业务模式,如上图中的出价设置模块,其内部还存在手动、MC、tCPA、eCPC等不同的出价模型,这个时候就需要根据分类思想进行进一步拆解。分类思想是关注点分离思想进一步的延伸,它在分离的同时还注重元素之间的共性特征。当模块内部出现场景分化时,PICASO框架建议开发者对模块进行进一步细分,将模块内不同场景的业务规则封装为不同的能力实例。这些能力实例之间尽管存在逻辑差异,但是毕竟属于同一个原子业务模块,在数据模型、接口协议乃至业务流程上都存在很大的相似度。因此PICASO会将同模块下不同业务场景对应的领域能力实例聚合到一起,这样的一组能力被称为一个能力节点。同一个能力节点下的各个能力实例使用相同的接口参数及上下文定义,每个能力节点下会额外定义一个能力门面,能力门面通常不承载具体的业务规则,它仅负责定义当前能力节点对外的接口协议以及从请求参数中提取业务场景标识的逻辑,它是能力节点下所有能力实例对外提供服务的统一入口。上层的领域服务组合领域能力时,引用的不是具体的领域能力实例,而是各个能力节点下的能力门面。PICASO框架内置的可执行实体发现与路由机制会在应用启动时扫描出系统中所有的能力门面,并建立好能力门面与各个能力实例的路由表。当请求到来时,领域服务不必关注本次请求应该使用哪个具体的能力实例,而是直接调用能力门面的统一入口,PICASO框架会通过内置的可执行实体发现与路由机制提取请求中的场景标识,然后将请求路由到对应的领域能力实例上,从而实现模块内部的场景复杂度与领域服务模块集成复杂度之间的解耦。以出价模块为例,出价模块内部会根据不同的出价类型细分为tCPA、MC、eCPC等智能出价能力实例,但是单元新建领域服务并不会直接操作这些具体的能力实例,它引用是出价设置能力门面。当请求到来时,PICASO框架会根据请求中的出价类型自动将请求路由到相应的能力实例上。可执行实体发现与路由机制是PICASO框架内置的一个底层通用组件,是能力编排、拓展点机制等顶层功能的基础。其本质上就是一个增强型的门面+策略模式,我们通过一些实现技巧将其做成了一个可以适配任意可执行实体的通用组件。如下图所示,单元新建业务流程涉及标的物设置、出价设置及人群设置等业务环节,这些业务环节内部都有各自的细分场景。在代码实现中,这些业务环节被抽象为3个能力节点,节点内部的细分场景被隔离到不同的能力实例中,在构建领域服务时就不需要考虑当前各个能力节点下的细分逻辑,只需要专注于业务流程本身,实现各个能力门面的组装逻辑即可。

能力门面与能力实例的抽象实现了能力编排复杂度的降维
分层
分层则是分类思想在领域服务、拓展点等其他实体粒度上的延伸。如快车、互动、推荐三条产品线的单元新建服务会被划分到同一个服务分组下,对外暴露一个单元新建服务门面。这样做目的是下层实体对上层实体暴露统一的门面接口,自下而上地逐层屏蔽下层实体的内部复杂度,实现维度间复杂度解耦,进而将代码的整体复杂度由

自下而上逐层屏蔽层级内部的业务场景复杂度
在分层架构中,除了可以通过通用可执行实体路由机制自下而上地屏蔽下层实体的内部场景复杂度之外,有时我们还要反过来自上而下地进行复杂度合并。我们用一个例子来说明这种设计技巧:在广告投放业务中有一个经典的出价计算器模块,它会根据广告物料上的基础出价、人群溢价、关键词出价、流量包溢价、时段溢价等信息预估广告物料最终的出价值范围。计算逻辑只有一个,但是由于计算逻辑关联了众多底层模块,物料新建、修改以及关联模块的快捷修改、还有对物料进行修改过程中实时出价预估回显(此时最新的修改并未落库)等接口都会调用出价计算器模块,但是这些使用场景对出价预估参数的填充程度是不同的,需要模块针对不同的使用场景执行不同的参数补充查询逻辑。这是一个典型的上层模块的调用场景复杂度渗透到底层模块实现复杂度中的例子。在分层架构中,越是底层的模块在设计上需要考虑的场景应该越少,而且要避免与上层模块的使用场景耦合。因为上层模块的使用场景是动态增加的,不知道什么时候就会有新的使用场景出现,而底层模块的真正需要处理的内部场景应该比顶层使用该模块的场景要少且稳定的多。所以解决这个问题的措施就是底层模块面向自己内部的业务模式在参数中定义一个隐式的标识属性,让调用方根据自己的使用场景和业务诉求隐式地设置该参数,底层模块则直接根据参数中的这个标识属性执行相应的分支逻辑。回到出价计算器的案例中,该模块的使用场景有:单元新建后事件触发、单元整体修改后事件触发、单元新增关键词后事件触发、单元新建中临时触发、单元修改中临时触发、单元添加关键词中临时触发等多种使用场景,未来也不确定会出现什么新的使用场景。但是对出价计算器模块的内部计算逻辑来说,其实只有需要补充查询单元下关键词信息和不补充查询这两种场景。为此我们在出价计算器能力参数中增加一个布尔类型的参数,能力内部直接根据该参数判断是否需要执行关键词的查询操作,出价计算器模块的调用方则分别根据自己的使用场景判断该如何设置这个参数,从而起到自上而下的合并上层调用场景复杂度、保持底层模块稳定的作用。
有些读者或许会觉得这种机制与上文介绍的能力路由机制是互相矛盾的,然而他们实际上并不冲突。因为对那些能够自下而上屏蔽内部场景复杂度的模块而言,它们通常显式地定义了内部不同业务模式的标识属性,如出价模块的出价类型、人群定向模块的人群类型等,用户在请求参数中也会显式地设置请本次请求对应的业务标识,因此框架能够直接对这些模块应用通用可执行实体路由机制。但是实际业务中也存在一些模块,它们内部没有定义明确的业务模式标识,而是根据请求来源、调用场景等动态条件执行不同的业务逻辑。此时我们可以先暂时忘掉这些模块的调用场景,而是聚焦模块内部的业务分支提炼出隐藏其中的业务模式,然后让上层模块将动态调用场景转化为底层模块定义的隐式业务模式标识参数,接下来就能继续应用通用可执行实体路由机制了。因此,分层思想中自下而上屏蔽的是模块内部的固有场景复杂度,而自上而下合并的则是模块外部的使用场景复杂度,二者其实是互补的关系。
拓展点机制协助走出多维度泥潭
领域服务与领域能力的路由机制能够较好的应对系统多模块集成带来的复杂度,但是领域能力及领域服务必须严格遵守框架规约,继承标准业务执行器模版(后续章节会有详细讲解),定义出明确的数据交换协议及上下文对象,这些都是相对较重的操作。因此能力或服务路由的维度必须抓住最核心的业务差异,而不是把所有存在业务差异的维度都纳入到路由规则中,否则就会造成沙粒化拆分,反而增加系统的维护成本。因此我们还需要一种机制能够以更加轻量的方式承载除了领域服务或能力路由维度之外其他业务维度上的细微差异,这就是拓展点机制要解决的问题。
拓展点机制是通用可执行实体发现与路由机制在更细粒度上的延伸应用,本质上就是将存在差异化逻辑的环节抽象为一个接口从主流程中分离出去,然后将不同场景的差异化逻辑隔离在不同的拓展点接口实现中,这其实就是依赖倒转原则(Dependence Inversion Principle, DIP)的应用。与领域服务和领域能力相比,拓展点的定义和实现成本都要低很多,框架对拓展点接口内的方法及方法参数都不会做过多的约束,定义一个拓展点仅需要继承框架提供的标准接口并指定路由标识的提取逻辑,而实现一个拓展点接口时也仅需要在实现拓展逻辑之外额外指定当前拓展点实例能适配哪些路由标识。拓展点可以嵌入到领域服务、领域能力以及资源库(Repository,下文中会详细阐述)中任何一处存在差异化逻辑的流程中。
拓展点机制作为能力与服务拆分路由机制的补充,支持任意维度上的差异化逻辑隔离。以下图为例,在广告投放系统中,底层的人群设置能力节点已经按照其核心属性人群类型进行了能力实例的划分。由于系统还赋能了多个投放平台,不同的投放平台对可绑定的人群上限有着不同的限制,此时就可以将各个能力实例中人群绑定数量校验环节抽象为一个拓展点接口,以投放平台类型作为路由KEY为各个投放平台提供不同的接口实现,从而自上而下地解决多维度拓展的难题。
这里说的“自上而下”是一种形象的描述,可以理解为父层级业务维度内不同的业务场景在子层级模块上产生的差异化逻辑。但实际上拓展点机制并不限制逻辑的维度拓展方向,如下图的例子中,右侧触点新建服务领域服务实例所属的服务门面定义的服务路由维度是产品线,但是不同的计划类型的单元新建流程之间依然存在细微的逻辑差异,此时尽管计划类型是产品线的子维度,但是依然可以通过拓展点来承载这些细微的逻辑差异。

拓展点机制的核心作用是作为能力及服务路由维度的补充,进一步实现差异点的分离
能力编排框架确保架构思想切实落地
前面几个小节一直在论述如何对复杂逻辑进行拆解和分离,但是系统要想对外提供可用的功能,就必须再次把这些分离出来的能力及拓展点组合起来,构成一个完整的领域服务。最简单的组合方式就是直接硬编码依次调用各个能力门面的功能入口,手动实现前置方法调用结果与后置方法入参的属性映射和转换,但是这种组合方式会在业务主流程中插入大量的胶水代码,稀释代码的信息密度,将流程关键节点掩盖在大量繁琐无趣的`setter`、`gettter`方法调用中。为了解决这个问题,同时确保新架构设计思想能够精准落地,让规范和标准框架化,PICASO自建了能力编排框架,它为前文所述的各类思想落地提供了框架基础,将前文提到的各种实体、组件与设计思想有机结合到一起,自动实现模块串联,让开发者专注于业务逻辑本身,实现填空式开发,最终构建出一个完整的工程应用。
目前业界有很多流程编排引擎,有老牌厂商的Netflix Conductor、AWS Step Function等,也有开源的Apache Activiti、Zeebe等。我们在早期架构探索阶段对这些解决方案也进行了调研和试用,但是发现它们都无法满足我们的诉求:以轻量级的方式实现模块组合,提高模块与组件的复用性,同时凸出呈现核心业务流程,辅助开发者快速抓住业务主线并建立对业务的全景认知。这很大程度上是由于上述开源组件的定位大都是接口级别的服务编排或者是审批流之类的流程引擎,因此其实现方案或执行成本往往较重,很多流程编排框架过分强调通过UI框架拖拽式实构建业务流程,导致开发者需要先在代码工程中实现业务组件,再到UI界面中构建串联流程,适用的场景有限且造成强烈的割裂感不说,开发者依然需要手动配置组件之间的参数映射与数据传递逻辑,而脱离了开发工具的代码提示与补全功能,这些逻辑的实现成本反而增大了。与这些问题相比,拖拽式的UI界面虽然炫酷,但并不是我们的核心诉求。还有一些编排框架采用了中心化的部署方式,流程串联与组件服务分离部部署,通过RPC实现组件调用,这种方式会付出巨大的网络开销及中间结果存储成本。这种设计让它们在批处理任务场景中有较好的应用,但是在交互式服务应用场景中则会造成严重的性能问题并付出巨大的运行成本。因此,在经过一次次尝试之后我们最终决定举起自研大旗,开发一套与PICASO架构基本思想相适配的能力编排框架。

要想满足我们在上文中提出的能力编排相关的诉求,能力编排框架需要提供两个基本功能:分别是在编码阶段通过简洁、直观、易用的API辅助开发者定义业务流程,以及在请求处理阶段根据开发者制定的执行图串联各个业务组件完成请求处理流程。为了实现这两个基本功能,PICASO框架采取了制定标准化业务执行模版、内嵌标准上下文机制以及自建能力编排框架三项举措。
标准业务执行器模版
标准业务执行器模版立足于软件系统的内在本质定义了适用任何业务场景的基本处理流程,就像Object对象在JDK中的作用一样,标准业务执行器模版并不复杂,但它却是PICASO框架中所有组件功能得以实现的基础。从本质上看,所有的软件系统都在做三件事:数据的获取、处理与存储(或传输);从业务视角看,数据的处理又可细分为输入数据的合法性校验以及数据的计算与转换,而数据的合法性校验又可细分为对输入数据直接进行的校验以及需要结合系统内外部详情数据进行的校验。基于上述论述,PICASO框架定义的业务处理的基本流程为:
标准业务执行器模版本质上就是一个Executor模板类,上述基本业务流程也就是该模板类中主要的模板方法。在PICASO框架中,领域服务和领域能力都要继承标准业务执行器模板类,这样做的目的是引导和约束开发者对领域服务和领域能力的具体实现逻辑按照标准业务执行流程进行二次拆分,从而可以让框架对代码进行精细化地调用控制。标准业务执行模版是对所有业务处理流程进行的最顶层抽象,模版类中各个标准业务执行步骤API的制定让把不同业务模块的串联执行职责从开发者手中转义到框架手中成为可能,开发者不必手动实现不同模块和方法的串联调用,而是专注于业务逻辑,实现填空式开发,从而减少系统中的胶水代码,提高信息密度,这本质上就是依赖倒转原则(Dependency Inversion Principle, DI)的应用。下面的代码片段给出了标准业务执行器模版的定义,出于突出呈现PICASO框架设计思想的目的,示例代码去除了框架功能的具体实现逻辑,仅保留了核心要素及模版方法的定义。
/**
 * 标准业务执行器模板基类,定义了基本的业务处理流程,所有领域服务和领域能力执行器都必须继承该类。 
 *
 * @param <C>   业务执行器对应的参数类型,所有的执行器参数都应该继承自标准参数基类Command对象
 * @param <T>   业务执行器最终返回的执行结果类型
 * @param <CTX> 业务执行器使用的上下文对象类型,所有执行器的上下文对象都应该继承标准上下文基类,
 *              请求的入参和产生的中间结果都会保存在上下文对象中
 */
public abstract class CommandExecutor
        <C extends Command, T, CTX extends ExecutorContext<C, T>> {
    /**
     * 参数预校验,该步骤应该只进行纯内存计算操作
     * @param context 上下文,此时的上下文中只有参数对象
     */
    protected Response<T> doPreValidate(CTX context) {
        return Response.success();
    }
    /**
     * 执行上下文初始化,根据参数执底层情数据的拓展查询,并将查询结果填充到context对象中
     * @param context 上下文,调用该方法时的上下文中只有参数对象,调用完成后上下文将被填充
     */
    protected Response<T> doInitContext(CTX context) {
        return Response.success();
    }
    /**
     * 结合上下文中的底层数据执行业务校验
     * @param context 上下文,此时的上下文中已经完成了依赖的业务详情数据的填充
     */
    protected Response<T> doContextualValidate(CTX context) {
        return Response.success();
    }
    /**
     * 结合上下文中的底层数据执行业务逻辑的处理,对已有实体的变更及生成的新业务实体都会填充回上下文对象中
     * @param context 上下文,业务逻辑执行过程中的中间结果也可以暂存到到该上下文中
     */
    protected Response<T> doProcessBizLogic(CTX context) {
        return Response.success();
    }
    /**
     * 保存业务流程执行过程中新建或者被修改过的业务实体,调用该方法时,这些数据已经被写入到了上下文对象中
     * @param context 上下文
     */
     
                     
                    
                