作者:京东零售 赵嘉铎
前言:信息时代技术更迭和传播速度不断加快,技术变得泛娱乐化,大数据、云计算、区块链、元宇宙、大模型,一代代技术热点在社会舆论的裹挟之下不断地吸引着资本的眼球,技术人员为了不被时代所淘汰也不得不时刻追赶潮流。在这样一个时代背景下,软件工程作为一门不起眼到有些枯燥的古老学科,似乎早已被开发者们遗忘在角落。作为一名技术人员我们自然应该时刻保持对前沿技术的追踪,然而,当发生线上问题我们却面对着成片的屎山代码毫无头绪时;当业务方提出个性化需求我们却因为不敢对系统做出修改而强迫对方做出妥协时;当一次请求处理流程中出现多达数万次重复地数据库操作而影响到整个系统的稳定性时,大家都应该沉下心来思考一下,我们是不是忘记了作为一名程序员的初心和对代码的极致追求。 还记的当年我抱着朝圣的心态从传统行业踏入京东职场时的兴奋与期待,然而这份期待很快就被四处可见的屎山代码给浇灭了,后来从朋友口中了解到其他头部互联网厂商的业务系统其实也是半斤八两。这似乎是软件行业中的一个电车难题,一边是无尽的业务需求和倒排的工期,一边是补丁摞补丁的糟糕代码,是继续泡在酱缸中缝缝补补还是向屎山代码说不,开发人员被困在中间不知该如何抉择。然而事实上,追求整洁架构与提升研发效率之间从来就不是一个悖论。正如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应用,广告投放系统长期以来采用的都是传统的三层架构,这种没有架构的架构极其简单、易上手,因此一直以来都是业界的主流。但是由于它缺少统一明确的逻辑拆分与封装工具,业务的复杂度会等比渗透到代码实现中,进而导致系统的代码复杂度飙升,模块之间随意耦合,逻辑纠结缠绕,经过几轮迭代之后就成了看不懂、动不了、不敢动的酱缸代码。这些看似基础的编码问题实际上却是阻碍我们研发效能提升的罪魁祸首:
1.需求交付提速困难:不同平台、产品线及业务场景逻辑交织,晦涩难懂,导致系统功能迭代时梳理及设计耗时漫长,同时在测试阶段需投入大量精力进行联动功能回归;
2.对新业态的接受度低、响应能力差:系统拓展性差,且模块间深度耦合,在面对新业态时我们却为了控制影响范围而不得让业务选择让步,结果错失商机。
3.问题评估和排查效率低下: 缺少明确统一的逻辑归属与封装准则,逻辑四处复写与逃逸,导致问题定位时间长,难以快速评估影响范围和修复方案。
4.接口性能与稳定性下降: 混乱的封装与复用导致一次接口请求就会产生导致大量重复的IO操作,严重影响接口性能,每临大促都需要花费大量人力进行性能优化。
主观:“面向数据库编程”的设计思维让系统加速腐化
我们的业务本质就是获取、处理、存储及传输数据,在传统架构中业务逻辑通常以事务脚本(Transaction Script)的形式实现:业务规则直接在开发者的大脑中转化为数据库的增删改查操作(这也是很多程序员调侃自己是CRUD工程师的原因),然后被写到代码里。这种模式在场景单一、需求简单的业务发展早期阶段可以快速实现功能,但是随着业务复杂度的提升,这种过于粗糙的设计思维所带来的问题就会逐渐显现出来:
1.难以建立对整个数据模型的全景认知:完整的数据模型信息被拆分到不同的业务接口实现中,往往需要对整个工程代码进行逐行review才能梳理出完整的数据模型,当工程代码量和数据模型膨胀到一定程度后,模型梳理成本急剧飙升。
2.模型野蛮膨胀、存在大量相似或重复的实体,增加系统运维成本:数据模型全景认知的缺失导致开发者难以进行统一的顶层设计,数据模型泛化表达能力弱,在多需求并行开发过程中极易形成信息孤岛,无法实现模型合并与共享,系统中存在大量相似的业务实体与库表结构。
3.代码对业务语义表达能力弱、业务知识传承效率低下:代码经过开发者的转译失去了对业务语义的直接表达,导致系统中存在大量只有开发者本人才能理解的魔法逻辑,系统维护与人员更迭成本过高。
要想解决上述问题,就亟需一种面向未来的架构思想来指导我们对系统进行全面地升级。在此背景下,业界众多平台纷纷进行了领域驱动设计思想的探索和尝试,经典的案例有阿里的星环与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个能力节点,节点内部的细分场景被隔离到不同的能力实例中,在构建领域服务时就不需要考虑当前各个能力节点下的细分逻辑,只需要专注于业务流程本身,实现各个能力门面的组装逻辑即可。
能力门面与能力实例的抽象实现了能力编排复杂度的降维
分层
分层则是分类思想在领域服务、拓展点等其他实体粒度上的延伸。如快车、互动、推荐三条产品线的单元新建服务会被划分到同一个服务分组下,对外暴露一个单元新建服务门面。这样做目的是下层实体对上层实体暴露统一的门面接口,自下而上地逐层屏蔽下层实体的内部复杂度,实现维度间复杂度解耦,进而将代码的整体复杂度由
降维到。下图展示了PICASO框架中各个业务实体的层级结构,最下层是各个领域能力实例执行器,它们承载了具体的业务规则;相同功能子域的领域能力实例会对上层的领域服务实例暴露一个统一的领域能力门面执行器,领域服务实例执行器会通过组合领域能力门面定义具体的业务流程;而相同的功能子域的领域服务实例又会对领域服务统一入口(Domain Service Faced)暴露一个领域服务门面执行器,屏蔽模块内领域服务实例之间的细分规则;领域服务统一入口将不同的领域服务门面集成到一起,对上层不同的流量来源暴露统一的请求入口。这种分层结构让开发者逐层解构业务复杂度的同时,实际上也构造了一个索引结构,为实现可检索的代码架构打下基础。
自下而上逐层屏蔽层级内部的业务场景复杂度
在分层架构中,除了可以通过通用可执行实体路由机制自下而上地屏蔽下层实体的内部场景复杂度之外,有时我们还要反过来自上而下地进行复杂度合并。我们用一个例子来说明这种设计技巧:在广告投放业务中有一个经典的出价计算器模块,它会根据广告物料上的基础出价、人群溢价、关键词出价、流量包溢价、时段溢价等信息预估广告物料最终的出价值范围。计算逻辑只有一个,但是由于计算逻辑关联了众多底层模块,物料新建、修改以及关联模块的快捷修改、还有对物料进行修改过程中实时出价预估回显(此时最新的修改并未落库)等接口都会调用出价计算器模块,但是这些使用场景对出价预估参数的填充程度是不同的,需要模块针对不同的使用场景执行不同的参数补充查询逻辑。这是一个典型的上层模块的调用场景复杂度渗透到底层模块实现复杂度中的例子。在分层架构中,越是底层的模块在设计上需要考虑的场景应该越少,而且要避免与上层模块的使用场景耦合。因为上层模块的使用场景是动态增加的,不知道什么时候就会有新的使用场景出现,而底层模块的真正需要处理的内部场景应该比顶层使用该模块的场景要少且稳定的多。所以解决这个问题的措施就是底层模块面向自己内部的业务模式在参数中定义一个隐式的标识属性,让调用方根据自己的使用场景和业务诉求隐式地设置该参数,底层模块则直接根据参数中的这个标识属性执行相应的分支逻辑。回到出价计算器的案例中,该模块的使用场景有:单元新建后事件触发、单元整体修改后事件触发、单元新增关键词后事件触发、单元新建中临时触发、单元修改中临时触发、单元添加关键词中临时触发等多种使用场景,未来也不确定会出现什么新的使用场景。但是对出价计算器模块的内部计算逻辑来说,其实只有需要补充查询单元下关键词信息和不补充查询这两种场景。为此我们在出价计算器能力参数中增加一个布尔类型的参数,能力内部直接根据该参数判断是否需要执行关键词的查询操作,出价计算器模块的调用方则分别根据自己的使用场景判断该如何设置这个参数,从而起到自上而下的合并上层调用场景复杂度、保持底层模块稳定的作用。
有些读者或许会觉得这种机制与上文介绍的能力路由机制是互相矛盾的,然而他们实际上并不冲突。因为对那些能够自下而上屏蔽内部场景复杂度的模块而言,它们通常显式地定义了内部不同业务模式的标识属性,如出价模块的出价类型、人群定向模块的人群类型等,用户在请求参数中也会显式地设置请本次请求对应的业务标识,因此框架能够直接对这些模块应用通用可执行实体路由机制。但是实际业务中也存在一些模块,它们内部没有定义明确的业务模式标识,而是根据请求来源、调用场景等动态条件执行不同的业务逻辑。此时我们可以先暂时忘掉这些模块的调用场景,而是聚焦模块内部的业务分支提炼出隐藏其中的业务模式,然后让上层模块将动态调用场景转化为底层模块定义的隐式业务模式标识参数,接下来就能继续应用通用可执行实体路由机制了。因此,分层思想中自下而上屏蔽的是模块内部的固有场景复杂度,而自上而下合并的则是模块外部的使用场景复杂度,二者其实是互补的关系。
拓展点机制协助走出多维度泥潭
领域服务与领域能力的路由机制能够较好的应对系统多模块集成带来的复杂度,但是领域能力及领域服务必须严格遵守框架规约,继承标准业务执行器模版(后续章节会有详细讲解),定义出明确的数据交换协议及上下文对象,这些都是相对较重的操作。因此能力或服务路由的维度必须抓住最核心的业务差异,而不是把所有存在业务差异的维度都纳入到路由规则中,否则就会造成沙粒化拆分,反而增加系统的维护成本。因此我们还需要一种机制能够以更加轻量的方式承载除了领域服务或能力路由维度之外其他业务维度上的细微差异,这就是拓展点机制要解决的问题。
拓展点机制是通用可执行实体发现与路由机制在更细粒度上的延伸应用,本质上就是将存在差异化逻辑的环节抽象为一个接口从主流程中分离出去,然后将不同场景的差异化逻辑隔离在不同的拓展点接口实现中,这其实就是依赖倒转原则(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框架定义的业务处理的基本流程为:
1.参数预校验:直接对请求入参进行的校验,这些校验逻辑通常都是简单的内存计算,不依赖任何外部数据,如参数完整性校验、参数值范围校验、数据长度校验等。
2.上下文初始化:基于校验后的入参查询数据详情并填充到上下文中,如根据入参中的单元ID查询单元详情、根据userId获取账户详情等,这些数据将会在后续流程中使用。
3.基于上下文的业务校验:执行需要结合上下文中详情数据才能进行的业务校验,如根据单元状态判断是否可以执行物料的修改操作、判断标的物类型与物料计划类型是否匹配等。
4.业务逻辑处理:基于参数及上下文中的详情数据执行领域模型(下一章节介绍)的构造和修改,注意对于一些查询类的服务,这个步骤可能不是必须的。
5.数据持久化:将新建或修改后的领域模型保存到数据库中或者调用外部服务API完成数据传递,这同样是一个可选的标准步骤,另外有些服务在业务逻辑处理环节就已经完成了数据传递。
6.发布领域事件:一些修改类的领域服务在完成请求处理之后可能需要通知其他领域内的业务实体做一些相关的后置操作,PICASO框架是通过领域事件机制来实现这个功能的(后续章节中进行详细介绍)。
7.构造处理结果:业务流程执行完成后构建返回给调用方的响应数据。
标准业务执行器模版本质上就是一个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 上下文
*/
public Response<T> doPersistAggregates(CTX context) {
return Response.success();
}
/**
* 构造本次业务请求流程中需要对外发布的领域事件
* @param context 上下文
*/
protected Response<Collection<RetryableEvent>> doPublishAppEvent(CTX context) {
return Response.success();
}
/**
* 构造请求的返回值
* @param context 上下文
*/
protected Response<T> doAssembleResponse(CTX context) {
return Response.success();
}
}
到这里有些读者可能还没有意识到“把不同业务模块的串联调用职责从开发者手中转移到框架手中”的价值,这项措施其实并没有直接解决我们在本文第二章提出任何一个痛点问题,要想理解这一措施我们必须用辩证法重新审视前文介绍的各项复杂度应对措施。根据前面几个小节的论述,我们为了应对业务复杂度而对请求处理流程进行了各种粒度和场景的拆分,拆分出来的各类实体再叠加上实体内部标准执行步骤的二次拆解,必然会增加后续逻辑串联和组装的复杂度。如果此时还要求开发者手动硬编码实现逻辑组装,那么势必会带来极高的开发负担和出错概率,而且硬编码组装带来的大量胶水代码还会稀释和掩盖代码中的关键信息,后续再进行迭代时就容易产生改动点遗漏和影响评估不全等问题。PICASO框架解决这些问题的措施就是由框架代替开发者实现合个模块的串联组装,这也我们要定义标准业务执行器模版的根本原因:统一标准的调用入口是让框架实现流程串联的前提,进而才可能实现将胶水代码隐藏在框架内部、提高业务层代码信息密度、降低开发者编码负担的设计目标。PICASO框架内模块串联的详细逻辑我们将在本章后三个小节中进行阐述,在那之前我们先继续介绍标准业务模版中的其他核心要素。
上下文机制
从标准业务执行模版的示例代码中我们可以看到,除了各个标准步骤的方法声明之外,标准业务执行模版还通过泛型变量定义了执行器接收的请求参数类型、返回值类型以及上下文对象类型,其中“上下文”是标准业务执行器模版中的核心要素,业务数据就是通过它在各个标准步骤之间流转的。所谓的上下文本质上就是一个POJO(Plain Old Java Object),其内部定义了业务流程执行所需要的各种详情数据。在《改进我们的架构》一文中我们已经对上下文机制进行了详细的阐述,在这里我们简单回顾一下它的作用。如下图所示,在传统架构中开发者往往直接面向数据库编程,业务逻辑与数据库操作互相交织,容易造成重复或碎片化的数据库读写操作。而采用上下文机制之后,业务流程中的各个子模块都不再封装数据的读写操作,而是在请求一开始先将后续流程所需要的数据集中初始化到上下文对象中,后续各个业务模块统一从上下文中获取所需的详情数据,并把产生的中间结果写入到上下文对象中,最后在所有子模块业务逻辑执行完成之后,集中将上下文中新增或发生变化的业务实体持久化到存储介质中。这种设计一方面能够避免子模块划分导致的重复及碎片化的数据读写操作,另一方面,集中的数据操作可以启发开发者采用批量、异步和并行等措施进行极致地性能优化。
上下文机制与传统架构业务处理流程对比
PICASO框架对上下文机制做了进一步升级和深度集成,上下文作为核心要素被直接定义到标准业务执行器模版中,领域服务和领域能力执行器都要通过泛型参数来声明自己所需的上下文对象类型。需要说明的是,尽管领域能力执行器中也定义了“上下文初始化”标准步骤,但是PICASO框架依然建议开发者尽量在领域服务执行器的上下文初始化步骤中就将各个领域能力所依赖的业务实体或外部数据集中批量查询好,然后填充到领域服务上下文中。后续各个领域能力会优先从领域服务上下文中获取所需的详情数据,领域能力的上下文初始化步骤仅做依赖数据的非空校验或者做为从领域服务上下文中获取不到所需数据时的托底补充查询措施,这是由于领域服务作为整个业务流程的全局把控者,拥有最全的数据视角,可以对代价昂贵的IO操作进行极致地调优,而领域能力则聚焦于业务流程的局部细节,在能力内部封装的数据读写操作很容易随着能力的组合或循环复用而被碎片化或重复地执行。
需要注意的是上文中“领域能力优先使用领域服务上下文中的数据”并不意味着领域能力会直接访问外层领域服务的上下文对象,这是由于同一个领域能力可能会被不同的领域服务所复用,因此领域能力不可能与其中任何一个领域服务的上下文耦合到一起。为了解决这个问题,PICASO框架要求每个领域能力都要定义自己专有的上下文对象。在调用领域能力之前先将领域服务上下文中的数据传递到领域能力的上下文中,领域能力中的业务逻辑直接访问的依然是领域能力自己的上下文对象,在能力执行过程中构建的新实体或者对已有实体的修改也会直接保存到领域能力上下文中。而在完成能力调用之后,PICASO会将领域能力上下文中新生成或者发生变更的属性传递回领域服务上下文中,从而在保持领域能力与领域服务解耦的前提下实现领域服务与领域能力上下文数据的共享。因此在PICASO框架中上下文机制除了起到避免碎片化及重复读写数据的作用之外,还负责在不同领域能力以及领域服务与领域能力之间进行数据的传递和共享。
我们可以用一个例子来详细描述上述机制,如下图所示,领域服务内编排了三个领域能力:A、B、C,其中能力A和C分别依赖业务实体1和实体4,能力B依赖能力A生成的数据实体2,完成业务逻辑处理后框架需要把能力B和C构建的业务实体3和5以及能力C对实体4的修改保存到数据库中。当请求到来时PICASO框架会首先调用领域服务的上下文初始化标准步骤(initContext
)完成实体1与实体4的查询,在调用能力A之前会将实体1从领域服务上下文拷贝到能力A上下文中,完成能力A的调用后会将其构建的实体2从能力A的上下文中拷贝回领域服务上下文,然后将领域服务的上下文作为两个能力之间数据共享和交换的通道,在调用能力B之前将实体2拷贝到能力B的上下文中......以此类推用相同的方式完成能力B和能力C的调用,最后PICASO框架会调用领域服务的聚合根持久化标准步骤(persistAggregate
),集中将新生成的实体3和5以及由能力C修改后的实体4持久化到数据库中。
领域服务与领域能力上下文之间的数据传递关系
上下文机制作为PICASO框架的基础组件,其内部除了用户自定义的业务属性之外还承载着大量框架内部运行所需的状态数据以及大量为开发者提供的工具API。在本文中我们仅对其基本运行机制进行了介绍,关于上下文的使用技巧及其基类中各种工具API的使用方法,我们将在《PICASO框架最佳实践——上下文机制》一文中进行详细的阐述。
标准业务模版执行引擎
在上一小节的最后我们通过一个架空的例子论述了PICASO框架内部的数据传递流程,这些数据传递规则并不需要开发者手动实现,而是通过PICASO框架内置的标准业务模版执行引擎自动触发的。接下来我们就将详细阐述标准业务模版执行引擎是如何将领域服务及领域能力各个标准步骤串联到一起的。但是在此之前,我们有必要必再次明确领域服务和领域能力执行器的职责,这对理解标准业务模版执行引擎的设计动机十分重要。
在PICASO框架中,系统对外提供的服务都是由领域服务执行器承载的,作为整个业务流程的全局把控者,领域服务执行器的基本职责就是定义业务流程(编排组装领域能力)以及管理业务数据(上下文的初始化及持久化),而领域能力执行器则聚焦在完整业务流程中的某个特定模块,负责实现该模块内部具体的业务规则。如前文所述,领域服务是通过领域能力组合编排而成的,并且它们都继承了标准业务执行器模版,因此不难推导出领域服步骤其实就是通过各个领域能力的相应标准步骤组合而成的。但是这并不意味着业务流程中所有的业务逻辑都会下沉到领域能力中,比如领域服务上下文初始化操作就必须在领域服务执行器中直接定义。此外,考虑到领域能力聚焦于局部业务细节,无法独立对外提供服务,为了明确组件职责,避免给开发者带来困惑,领域能力执行器对标准业务执行器模版进行了二次拓展,隐藏了完整请求流程处理维度才需要的聚合根持久化(persisteAggregates
)、构建并发布领域事件(publishAppEvent
)以及组装请求响应数据(assembleResponse
)标准步骤,因此这三个模版方法对应的业务逻辑也需要直接在领域服务执行器中定义。
领域服务及领域能力执行器的职责划分决定了二者之间的数据传递时机及其标准步骤之间的组合关系,标准业务模版执行引擎的模块串联规则就是基于此制定的。其核心设计就是对业务流程中各个领域能力标准步骤的重组执行。在PICASO框架的早期摸索阶段,我们曾倾向于将同一个业务模块的参数校验与业务处理逻辑划分到两个不同的领域能力中。这种能力划分方式固然也能实现业务功能,甚至也能起到复杂度分离的作用,但是这种划分方式会造成业务逻辑的沙粒化分解,产生大量琐碎的小能力,这反而会增加系统的开发及维护成本。另外,由于同一个模块的参数校验及业务处理逻辑往往会依赖相同的底层数据,沙砾化的能力划分会急剧增加能力间数据传递和共享的负担,稍有不慎就会造成数据的碎片化读写,进而对系统性能产生影响。因此我们最终选择回归业务本质,以最小原子业务边界作为能力划分的准则,将同一模块内关联紧密的参数校验、上下文初始化、上下文校验及业务处理逻辑封装到同一个领域能力执行器中。但是这种封装规则却带来了新的问题:领域服务由领域能力组合而成,如果我们直接依次串行调用每个领域能力内的各个标准步骤,将无法实现领域能力与领域服务标准步骤之间的协调执行,另外由于调用后置能力时前置能力所有标准步骤都已执行完毕,如果后置能力的参数校验失败而前置能力在业务逻辑处理步骤已经与外部系统产生了数据交互,此时就会产生脏数据等问题。然而标准业务执行模版的抽象则为我们带来了该问题的解决方案:将领域能力执行器中的各个标准步骤拆散到领域服务执行器相应的标准步骤中重组执行,而不是依次触发每个领域能力的全部标准步骤,如下图所示,在领域服务的参数预校验标准步骤中会按照能力执行图依次触发各个领域能力的参数预校验步骤,而在领域服务的上下文初始化步骤中则会依次触发各个领域能力执行器中的上下文初始化步骤。这种重组执行机制确保了服务请求流程能够整体按照参数预校验、上下文初始化、上下文校验、业务逻辑处理、聚合根持久化、发布领域事件、构造返回值的标准流程执行下去,实现fail-fast
特性,避免由于后置操作校验失败而前置操作已执行导致的IO资源浪费及脏数据问题,另外这种运行机制带来的额外收益是让我们能够利用现有服务快速实现请求预校验接口,这一点我们在本文第四章的PICASO框架开发流程示例中将有专门的呈现。
上述过程也是辩证法的生动诠释,事物之间存在普遍联系,在对立统一中不断发展。PICASO框架中也是在一次次提出方案、引发新问题、解决新问题的过程逐渐成型的,框架中各个组件互相支撑,互为因果,共同实现整洁架构的最终目标。我们也希望各位读者在阅读本文时能够始终将本文介绍的各项组件联系到一起来理解框架的指导思想和设计动机,这对未来我们能否在实际业务用好PICASO框架来说十分重要。
标准业务模版执行引擎的重组执行流程
下面我们将结合着上图所示的能力标准步骤重组执行流程图逐步解析标准业务流程模版执行引擎的运行机制:
1. 参数预校验
当请求到来时,PICASO框架会首先通过领域服务门面定位到具体的领域服务执行器实例,然后调用其参数预校验标准步骤(preValidate),在该方法中会首先执行领域服务执行器直接定义的参数预校验逻辑(当然也可以根据开发者的设计意图调整为先触发各个领域能力的参数预校验逻辑),然后再触发领域能力执行图中各个领域能力的参数预校验逻辑,需要注意的是由于领域能力执行器有自己专属的参数及上下文对象,因此在调用各个能力参数预校验方法之前,PICASO会自动将领域服务入参对象中的属性拷贝到领域能力入参对象中同名同类型的属性上(与Spring框架中BeanUtils.copyProperties的逻辑相同)。
2. 上下文初始化
完成参数预校验逻辑之后,PICASO会开始执行领域服务的上下文初始化逻辑。我们鼓励开发者将各个领域能力所依赖的底层数据集中到领域服务的上下文初始化逻辑中批量查询好,因为领域服务作为整个业务流程的全局把控者,拥有最全面的数据视角,可以进行最彻底的性能优化。完成领域服务直接定义的上下文初始化逻辑之后,PICASO将调用能力执行图中各个领域能力的上下文初始化步骤,但是在此之前,与领域服务与领域能力之间的参数传递逻辑类似,PICASO框架会先将领域服务上下文对象中的属性拷贝到领域能力上下文对象中同名同类型的属性上。
有些读者可能会对本小节的论述有些疑惑,封装领域能力的目的之一是为了逻辑复用,然而我们却要将其依赖数据的初始化逻辑代理到领域服务中,那么当一个领域能力被不同的领域服务引用时,是否会造成重复编码呢?这个问题的答案是肯定的,但是绝大多数场景下领域能力依赖的底层实体通常不多,一方面我们可以通过接下来将要介绍的“聚合与资源库”机制简化这些底层实体的查询逻辑,与由此收获的性能提升收益相比,重复编码所付出的轻微代价是完全值得的。另一方面我们其实并不建议在领域能力内部实现依赖数据的初始化查询操作,因为能够被编排到同一个领域服务中的领域能力通常都会依赖相同的业务实体,如果要在每一个领域能力内都实现一遍实体查询逻辑同样会造成重复编码。因此我们建议还是统一由领域服务完成上下文的初始化,然后通过上下文传递机制拷贝到领域能力上下文中,领域能力仅在上下文校验标准步骤中做好上下文参数的非空校验,确保领域服务传递过来了正确的数据。更多关于领域服务及能力上下文数据传递方案设计的技巧请参考《PICASO框架最佳实践——上下文机制》。
3. 上下文校验
完成领域服务及各个领域能力的上下文初始化逻辑之后,PICASO会继续执行领域服务及各个领域能力的上下文校验逻辑。该标准步骤内执行的是需要结合上下文中的底层数据才能进行的校验逻辑,如调整预算时要求新预算与历史预算差值必须大于5%且必须大于当前消耗,该逻辑依赖历史预算及物料当前消耗详情,就可以在上下文初始化步骤完成这两部分底层数据的查询,然后在上下文校验步骤中直接从上下文中取出详情数据执行相关的校验规则。默认情况下领域服务与领域能力在上下文校验步骤不需要执行任何的参数或上下文传递操作。
4. 业务逻辑处理
基于上下文的业务校验通过之后,PICASO框架会继续触发领域服务及能力执行图中各个领域能力的业务处理逻辑。需要注意的是由于领域能力的业务逻辑处理过程中可能会对上下文中已有的实体进行了修改,也可能会构建出新的业务实体对象,这些变更最终都需要被持久化到存储介质或者外部系统中。因此在默认情况下,PICASO会在能每一个领域能力的业务逻辑处理标准步骤执行完成之后执行一次领域能力上下文到领域服务上下文的数据回传操作,将领域能力上下文中的属性拷贝到领域服务上下文中同名同类型的属性上。这些数据将在领域服务的后续步骤中被持久化到存储介质中,或者被用于构造领域事件及请求响应结果。
6. 聚合根持久化、发布领域事件、构造响应结果
由于标准业务执行器中的剩余的几个标准步骤承载的都是请求维度的逻辑,领域能力执行器标准模版中对这几个方法也做了屏蔽,因此在这几个标准步骤的执行流程中就不需要再调用领域能力执行图了。需要特别说明的是,当执行到聚合根持久化标准步骤时,定义在领域服务及领域能力中的业务规则对实体的变更以及构建出的新业务实体都已经写入到了上下文中,开发者可以充分利用领域服务对数据操作全局把控的职责定位,积极采用批量、异步、并行等手段进行极致地性能优化。
7. 定制化执行流程
前五步内容介绍了领域服务及领域能力标准执行模版默认的串联执行逻辑,PICASO框架也遵循约定大于配置(convention over configuration)的基本原则,如果默认的执行逻辑能够满足开发者的诉求,开发者不需要实现过多的流程控制,但是要更灵活地适配各类业务场景,PICASO框架也支持开发者对上述标准串联执行逻辑进行定制化的修改:
•首先,PICASO允许开发者指定仅执行领域服务的部分标准步骤,如前台业务方期望能够在实际调用系统的单元创建接口之前先对其构造出来的请求参数进行校验提前发现问题,因此希望系统为其提供一个预校验接口(注意这里的“预校验”不是标准执行模版中的参数预校验步骤),该场景就可以直接复用物单元新建领域服务执行器,并且在触发领域服务执行器时指定间仅执行该领域服务的参数预校验、上下文初始化及上下文校验逻辑,领域服务完成前三个标准步骤的执行之后就会立即返回前三步的执行结果,从而快速实现业务方诉求,这其实也是标准业务执行模版抽象带来的额外收益。
•其次,PICASO框架支持开发者对领域能力执行图内的参数及上下文传递的时机与具体映射逻辑、能力调用的失败与异常处理以及各个领域能力的触发时机等行为进行定制化的修改,如出价设置能力与预算设置能力都依赖物料当前的消耗数据,除了在领域服务的上下文初始化步骤完成查询的常规设计之外,也可以让出价设置能力完成消耗数据查询,然后以领域服务上下文作为媒介,将物料消耗数据从出价设置能力传递到预算设置能力中。这个时候就可以指定PICASO框架在完成出价设置能力的上下文初始化步骤调用之后立即执行一次从能力到领域服务上下文的数据回传操作,而不必等到默认的业务逻辑处理步骤完成之后。而这些定制化的串联执行配置都可以通过接下来将要介绍的能力编排领域特定语言来实现。
能力编排执行图
在前一小节中我们介绍了PICASO框架内部各个原子模块与标准步骤的串联执行流程,PICASO框架通过内置的标准业务模版执行引擎将各个模块的串联执行职责从开发者手中转移到了框架内部,从而让开发者专注于业务规则设计,实现填空式开发。这里说的“业务规则”一方面是指领域服务与领域能力各个标准步骤内具体的业务逻辑,另一方面是要明确当前业务流程需要按照什么样的顺序执行哪些领域能力、能力执行的前置条件、对默认串联规则的定制化配置(包括参数传递规则、上下文传递规则、错误及异常处理逻辑等),这些信息将以领域能力执行图的形式提供给PICASO框架,之后框架就可以按照开发者的意图完成对各个领域能力的串联调用,而能力编排指的就是构建领域能力执行图的过程。
PICASO能力编排框架的核心职责有两个,首先是在编码阶段让开发者能够以易用、简洁、直白的方式快速定义出业务流程对应的领域能力执行图,其次是在请求处理阶段将能力执行图解析为可以被标准模版执行引擎理解的执行计划,让其能够根据开发者意图完成业务逻辑的处理。我们可以通过下图所示的框架内部实体关系图对上述两项职责进行详细阐述,图中蓝色线条标记的是领域能力执行图的构建过程,红色线条标记的是请求到来时领域能力执行图的执行流程。
PICASO能力编排框架内部实体关系图
上图其实不算是标准的实体关系图,它更像是实体关系图与流程图的结合,其中不同颜色的线表示不同执行流程。事实上我们认为这种呈现方式更加符合现实,实体之间的关系本就是复杂的,在不同的场景和流程下实体之间的关系和相互作用往往也是不同的。
从图中我们可以看到,每一个领域服务执行器内部都集成了一个领域能力编排器,在编码阶段,开发者可以通过它提供的领域特定语言(Domain Specific Language, DSL) 以直白的方式构建领域能力执行图。能力执行图由多个领域能力编排节点构成,每一个领域能力编排节点内部都封装着一个领域能力门面执行器。当请求到来时,业务模版执行引擎会首先对能力编排执行图进行解析,根据本次请求的参数及上下文信息将执行图中各个能力编排节点解析为零到多个领域能力执行要素,所谓的领域能力执行要素就是一个 [领域能力执行器、参数对象、上下文对象] 三元组,它是一个有状态的、会话级生命周期的实体,除了核心的能力执行三要素之外,其内部还维护着在请求处理过程中执行引擎产生的一些控制中间状态,如当前已执行到哪个标准步骤、调用过程中是否发生了异常、本次调用是否已经提前终止等。执行图中解析出来的所有领域能力执行要素将被构造成一个领域能力执行要素链调用器,它用来控制各个领域能力执行要素的触发行为,包括能力执行器各个标准步骤的逐步调用、能力编排节点的延迟解析、不同执行要素之间的并行调用、能力执行要器的提前终止等。在领域能力执行要素链调用器的控制之下,能力执行图中的各个能力门面执行器被依次触发,通过标准可执行实体发现与路由机制定位到当前请求应该使用的能力实例,调用其各个标准步骤完成业务逻辑处理。由于本文旨在介绍PICASO框架中各项组件的基本原理和运行机制,并没有对能力编排框架的实现细节做过多探讨,有关能力编排框架各项特性的详细介绍请参考《PICASO框架最佳实践——能力编排》。
能力编排领域特定语言
在PICASO框架设计之初,我们也曾想直接引入一些开源的流程编排框架来实现领域能力之间的串联调用。但是正如本章节最开始论述的那样,现有的开源解决方案并没有满足我们的核心关切:以直白、简洁、轻量、易用的方式实现能力组装,解决为了应对业务本质复杂度而采取的各项实体拆分与路由机制带来的编码繁琐、模块组装逻辑复杂等副作用,减少胶水代码和开发者的编码负担,提高关键业务信息密度。图形化、配置化的流程编排框架虽然能够直观的呈现业务处理流程,但是也造成灵活度差、普适性低、开发流程割裂、业务知识分散、模块串联配置繁琐等问题,无法达成整体熵减的设计目标,而PICASO框架解决这些问题的方案则是自定义能力编排领域特定语言。
领域特定语言(Domain Specific Language, DSL) 是专门针对特定应用领域的计算机语言,与C++、Java等通用计算机语言(General Purpose Language,GPL)相比,领域特定语言的功能及普适性十分有限,但是在特定领域之内它却具有强大的表达能力。领域特定语言的核心吸引力在于它提供了一种更清晰地传达系统各部分意图的方法,提高代码的可读性(虽然我们总是有意或者无意地低估了代码可读性对生产力的影响),降低开发者与领域专家(产品、测试甚至是用户)的沟通难度。它能够让使用者轻松实现声明式编程(Declarative Program) ,对业务层开发者来说,这意味着他们可以直接告诉框架他们想做什么,而不必编写要想达成目的而需要执行的具体操作步骤(Martin Fowler, Domain Specific Language, 2013),也正是这一点让PICASO框架能够将前文所述的各项业务复杂度应对措施带来的系统偶然复杂度屏蔽在框架内部,将PICASO框架内部的各个功能模块有机结合到一起,共同实现整体熵减的设计目标。
软件工程领域的大师Martin Fowler将领域特定语言分为外部DSL(External DSL)和内部DSL(Internal DSL)两大类。外部DSL往往拥有自定义语法、需要宿主应用的代码执行文本解析,基于该类DSL编写的业务规则通常以脚本或配置的形式存在于系统代码之外,典型的案例是正则表达式。而内部DSL是通用编程语言的子集,它对外提供一组特定的API,利用内部DSL编写的业务规则往往是一段合法的代码,典型的例子就是JDK8之后提供的Java Stream API。与外部DSL相比,内部DSL不需要专门的语法解析器和开发平台,可以直接与宿主应用代码无缝衔接,也能直接复用普通IDE的代码提示与自动补全功能,也正因为此,为了向业务开发者提供集中、连贯的开发体验,我们最终选择为PICASO能力编排框架开发一套内部领域特定语言。
为了尽可能灵活地适配所有的业务流程构建场景,我们在PICASO框架的能力编排DSL中定义了顺序、条件和循环三套能力编排逻辑,分别对应顺序执行、if...else
判断、循环三种流程控制方式。其中每一类能力编排节点的配置都遵循约定大于配置的原则,按照标准业务模版执行引擎的默认执行逻辑提供了全部缺省配置,同时开发者也可以通过能力编排API定制自定义的执行逻辑,如循环规则、分支判断条件、触发能力时的参数及上下文传递逻辑、失败及异常处理逻辑、能力节点解析步骤等。由于本文旨在介绍PICASO框架的设计思想和各模块的底层运行机制,因此我们不会对能力编排框架所有DSL API进行详细论述,这部分内容将在《PICASO框架最佳实践——能力编排》一文进行详细论述。本文仅通过一个实际的能力执行图构建案例让大家对能力编排DSL有一个具象的感知。
下图给出的是一个站外字节广告计划创建请求处理流程对应的领域能力执行图构建逻辑,可以看出能力编排DSL仅用数十行代码以一种近乎白话文形式描述出了完整的计划构建过程:首先对用户已创建计划数量进行上限检查(CampaignUpperLimitCheckAbility
);上限校验通过后会构造出一个空的计划对象并为其填充用户ID、计划类型等基础信息(CampaignBaseInfoAssembleAbility
);然后判断本次计划创建请求是否在参数中设置了联合活动ID,如果联合活动ID不为空则需要执行联合活动信息设置相关的业务逻辑(CampaignJointActivityAbility
);完成联合活动信息设置之后就要依次设置计划的投放周期(CampaignScheduleConfigAbility
)、计划名称(CampaignNameConfigAbility
)、营销目标(CampaignMarketTypeConfigAbility)、应用集(TrafficStrategyConfigAbiliy
)等模块的相关属性,需要注意的是在上述几个能力的编排逻辑中,由于领域能力的参数或上下文对象中的属性名称与领域服务的参数及上下文中相应属性并不匹配,默认的参数及上下文数据传递机制将无法为这几个属性设值,因此开发者对营销目标设置能力的参数传递(cmdTransfer
)、上下文传递(ctxTransfer
)、应用集设置能力的上下文传递(ctxTransfer
)逻辑进行了自定义拓展,手动实现了参数及上下文中特殊属性的数据映射逻辑;在完成这些业务模块的属性设置之后,如果请求参数中设置的标的物类型为“商品库”,那么接下来就要执行与站外DPA广告业务相关的特殊业务环节:推广SKU的校验(SkuValidateAbility
)以及SKU跟单信息的配置(TraceOrderSkuConfigAbility
)逻辑......
顺序及条件能力编排DSL示例
上图计划新建流程的能力编排逻辑中只用到了顺序和条件编排这两种最常用的编排模式,但是在一些批量操作请求处理流程中通常还会用到循环编排模式,它允许标准业务模版执行引擎重复调用同一个能力门面实现批量请求的处理。下图通过批量创意绑定接口给出了循环能力编排模式的使用示例,在该流程的最后一个环节通过registerFlatMapped
能力编排API注册了一个创意绑定能力(CreativeBindAbility
)。该能力被设计为处理单个创意的绑定操作,而批量创意绑定领域服务却需要在一次请求中完成多个创意的绑定操作,因此我们通过cmdFlatMap
能力编排API定义了将领域服务参数中的待绑定创意列表展开成多个单创意绑定能力参数的规则,标准业务模版执行引擎将据此遍历每一个单创意绑定参数并调用单创意绑定能力进而实现批量创意绑定。从示例中我们还可以看到,开发者在编排创意绑定能力时还通过transferCtxBeforeStep
能力编排API指定在调用领域能力的上下文初始化和业务逻辑处理两个标准步骤前都执行一次领域服务到领域能力的上下文传递操作,而标准业务模版执行引擎的默认行为是仅在领域能力上下文初始化标准步骤调用前执行该操作。除此之外,开发者还通过onFailure
/onException
能力编排API定制了失败及异常处理措施,确保在循环过程中单个创意绑定失败或异常不会中断整个业务处理流程,而是在处理完所有待绑定创意之后,将绑定失败的创意信息及失败原因返回给调用方。
循环能力编排DSL示例
有些读者可能会疑惑为什么不把创意绑定能力设计为直接在能力粒度上就支持批量绑定逻辑,这是因为当前的设计是综合考虑能力划分原则、创意绑定业务实际与拆分规则收益等因素之后决定的。我们曾在前文中提到,领域能力封装的是最小原子业务模块,而批量处理实际上属于流程控制逻辑,因此从职责划分的角度考虑,领域能力沉淀单个创意绑定的具体业务规则、领域服务负责循环流程控制的设计更符合PICASO框架的底层设计逻辑;另外从业务实际来分析,由于不同创意类型对应的创意绑定逻辑也有差异,因此创意绑定能力会按照创意类型拆分为不同的能力实例,而且广告主有可能通过批量绑定接口一次性绑定类型不同的多个创意。在这样的业务背景下,单创意绑定的能力拆分逻辑可以让领域服务直接遍历每一个待绑定创意,然后通过能力门面将单个创意绑定请求路由到与待绑定创意类型相适配的能力实例上进行处理,而不需要执行按创意类型分组等预处理逻辑;最后从拆分收益上看,单个创意绑定的拆分逻辑能够给我们提供更多的编排灵活性,通过定制化的失败及异常处理逻辑,不同的领域服务可以灵活地支持快速失败或允许部分失败等异常处理规则。综上考虑我们最终采用了单创意绑定的能力拆分规则,而这个决策过程其实也是我们进行能力拆分的一般流程。从这个例子中我们可以发现,能力拆分其实是一个主观性很强的行为,尽管我们为其制定了一系列的指导准则,但是在实际需求中开发者依然需要充分发挥主观能动性,在充分理解新架构设计思想的前提下,紧贴业务实际,在系统性能、架构整洁程度以及实现成本之间找到最佳的平衡点。
截止到目前,PICASO领域能力编排框架已经经历了两轮功能迭代,前文所述的顺序、条件及循环编排是第一代能力编排框架提供的特性,这一代能力编排框架仅支持领域能力之间编排串联,而领域服务各个标准步骤内的业务逻辑和能力执行图之间的串联依然需要手动硬编码实现。下图给出了一个使用第一代能力编排框架的领域服务案例,这个案例也清晰地呈现了领域能力执行图与领域服务各个标准步骤之间的关系。可以看到领域服务执行器提供了一个标准的领域能力编排入口registerDomainAbilities
,该模板方法通过参数提供了一个领域能力编排器(DomainAbilityOrchestrator
),开发者可以通过该模板方法完成领域能力执行图的构建。然而当请求到来时,标准业务模版执行引擎并不会直接触发领域能力执行图的调用,它仅会按照标准业务执行流程依次调用领域服务执行器的各个标准步骤,而领域能力执行图的调用则必须由开发者硬编码到领域服务执行器的各个标准步骤中。当然框架已经将领域能力执行图的触发逻辑封装成了相对易用的API(详见下面截图中doPreValidate
方法中被标注的代码片段),因此这种设计的实现成本不算太高。但是随着系统中领域服务执行器数量的增长,它也的确在系统中引入了大量重复的胶水代码,并且造成了领域服务中业务逻辑与领域能力值执行图之间的割裂,无法提供彻底的连贯开发体验。
第一代能力编排框架依然存在一些问题
近年来,为了给广告主提供简洁易用的投放体验,系统正越来越多地向着智能化和集成化的方向发展,让广告主少操作、少输入成为UI交互设计重要原则。在这样的业务背景下,我们收到了越来越多的跨层级接口合并需求,这让我们的业务在模块化的基础上又呈现出集成化特征,而且很多情况下,这种集成是“跨层级”的。如过去我们的广告物料一直遵循经典的计划、单元、创意三层结构,计划下可创建多个单元、单元下可绑定多个创,创建物料时需要依次调用三个接口。然而在近期全站推广、一页投放等需求都有一键创建全层级物料的诉求。但是由于子层级物料的创编流程都依赖父层级的物料对象创建完成,这与第一代能力编排框架中的重组执行机制底层逻辑相悖;另外,由于父层级物料对象与子层级物料对象之间都是一对多的关系,在一键创建全套物料的场景中,我们需要循环调用子层级物料的构建流程,然而第一代能力编排框架中循环能力编排特性仅支持循环调用单个领域能力,而不支持对多个领域能力执行链的重复触发。
为了更好地适配业务发展趋势,同时解决上文提到的第一代能力编排框架中由于领域服务与领域能力执行图之间逻辑分离定义、需要手动硬编码完成串联造成的业务逻辑割裂、会引入大量重复的胶水代码等问题,我们在第一代能力编排框架的基础上进行了升级,引入了子流程编排机制。子流程编排最重要的升级是增加了分阶段集成的特性,这也与当下多接口集成的业务发展趋势相符,一个完整的请求处理流程可以被拆分为不同的执行阶段,后置阶段的执行逻辑可能依赖于前置阶段的执行结果。PICASO框架允许开发者通过子流程编排DSL将一个完整的业务流程拆分定义为多个业务处理阶段,其中每一个业务处理阶段被称为一个子流程,每一个子流程可以看做是一个小的领域服务,有各自专属的标准处理步骤及领域能力执行图。除了可以像第一代能力编排框架那样为每一个子流程定义专属的领域能力执行图之外,第二代子流程编排框架还让开发者不需要再去重写领域服务执行器标准模板中的各个标准步骤,而是通过子流程编排DSL将每个子流程专属的参数预校验、上下文初始化、上下文校验及业务逻辑处理四个标准步骤与领域能力执行图一起直接定义到子流程执行图中,从而彻底解决了第一代能力编排框架中领域服务维度的业务逻辑与能力执行图开发割裂的问题。也正因为如此我们在第二代子流程编排DSL中将第一代编排框架中的领域能力编排器(DomainAbilityOrchestrator
)升级成了领域服务构造器( DomainServiceBuilder
) 。
下图给出了一个使用子流程编排DSL构建领域服务执行图的例子,可以看到,与第一代能力编排框架类似,第二代子流程编排DSL为开发者提供了顺序、条件、循环、捆绑及包装五种子流程编排节点。其中循环子流编排节点支持子流程维度的循环调用,解决了第一代能力编排框架中FlatMap
编排节点仅支持对单个能力进行循环调用的问题,因此在一对多的子层级物料创编场景中有广泛的应用,如下面例子中的在单元下绑定关键词列表的场景:一个单元下可以绑定多个关键词,而单个关键词的绑定操作需要串联执行多个领域能力,此时我们可以将单个关键词的构建过程定义为一个基础顺序子流程,然后再通过循环子流程编排节点将这个基础子流程循环集成到单元创建领域服务执行图中,这样就可以实现在单元下批量绑定关键词的功能;而捆绑子流程的作用类似于通用编程语言中的‘{}
’,它能够将多个子流程包装成一个逻辑子流程节点,这一特性让子流程编排能够支持任意不同子流程之间的组合及嵌套,进一步提升了编排框架对各类业务场景的普适性;包装子流程节点则是专门为已有领域服务执行器的组合复用而设计的,它可以将一个现有的领域服务执行器包装成一个子流程编排节点添加到子流程编排执行图中,这一特性在复用现有接口进行集成化改造或实现批量操作等需求中会为开发者提供极大的便利。
第二代子流程及能力编排框架示例
通过上面的几个例子我们能感受到,领域能力编排DSL能够以极高的信息密度描述业务流程的关键信息,通过构建领域能力/子流程执行图的方式定义业务流程的措施不仅能够减少胶水代码、降低开发负担,还可以协助开发者快速建立起对业务的全景认知,同时,领域能力/子流程执行图也能作为详细业务规则的目录或索引,在进行业务梳理或问题排查时让开发者可以按图索骥快速定位目标代码的大致位置,提升业务知识传承及问题定位的效率。
尽管我们在PICASO框架中舍弃了图形化流程编排框架的设计,但是我们并没有否定它存在的意义,这种编排方式在低代码编程领域占有重要的地位。但是它更适合在一些节点类型有限或者流程相对稳定的业务场景下使用,比如审批流程,虽然审批流可能有很多,但是每个审批流程中的节点种类往往不多,可以用相对固定的模式进行串联;或者广告播放流程,尽管其业务流程同样节点众多、冗长复杂,但是流程数量相对固定,基本上可以分为展示、搜索、推荐、站外、合约这五大核心流程,很多变更是对流程的微调或节点内部的逻辑升级。与这些业务相比,广告投放系统则维护了近300个能力节点、400多条请求处理流程,每次需求迭代都会涉及数十条业务流程的变更。在这样的业务特点下,简洁、灵活、集中、连贯的开发体验要比一个炫酷的UI交互界面重要的多。当然,我们依然十分认可图形化界面强大的呈现能力,事实上我们也在规划为PICASO框架开发内置的代码元数据管理平台,其中一项重要的功能就是把代码中基于能力编排DSL构建出来的领域能力执行图解析为业务流程图回显到交互界面中,让开发者可以直观地查看所有领域服务的处理流程。但是该平台只做能力编排逻辑的图形化展示,领域能力执行图的构造依然是通过代码中的能力编排API实现的(当然该平台也可以提供各个能力编排节点配置属性的动态修改功能,但是这仅作为紧急情况下的应急处理措施)。
PICASO的愿景:构建图书馆式代码架构
至此我们已经完成了对PICASO框架全部核心模块的介绍,此刻让我们再次回首PICASO框架的设次初衷——竭尽所能地提升团队的研发效率,这也是一线业务开发团队的核心价值所在。
提到研发效率,有很多同学认为少写代码就是研发效率高,也有同学认为支持了配置化、使用了拓展点就能实现研发效率的提升。然而事实上代码行数从来就是不是制约研发效率提升的核心要素,对业务问题的抽象和封装有时的确会导致我们多写一些代码,但是与多写几行代码花费的几十分钟相比,在实际工作中我们会把更多的时间和精力消耗在确定分工时与合作团队的扯皮上、需求设计时各团队方案对齐及历史业务逻辑的确认上、出现问题时原因的定位与影响范围的评估上、由于自己或者上下游系统的技术或架构限制而不得不进行的妥协方案开发上......解决这些问题所花费的时间才是阻碍研发效率提升和耗尽业务方对研发团队信任的根源。至于配置化和拓展点,不可否认的是它们的确是包括PICASO框架在内的很多效率提升解决方案中经常采用的措施,但是如果上述问题得不到解决,这些措施也只能是治标不治本。正如“鲍勃大叔”Robert C.Martin在其著作《架构整洁之道》中所说:“不管你多敬业、加多少班,在面对烂系统时你仍然会寸步难行,因为你大部分的精力是在应对混乱。”而我们之所以要为分工扯皮、之所以不敢升级系统以适配新的业态、之所以无法快速梳理出业务规则,都是因为过去大泥团式的代码架构让我们看不懂、不敢动、动不了。
为了解决上述问题,PICASO框架在设计之初就确立了框架即规范、代码即文档的基本原则,从设计阶段就开始引导开发者以统一的思想和方法论对业务进行拆解分析,然后将业务规则淀到标准业务执行模版或拓展点接口中,确保每一处业务规则都有可检索、可引用的实体边界;接着通过能力编排框架以近乎白话文的方式将这些原子业务实体组装起来,以极高的信息密度清晰地描述完整的业务流程;最后通过通用可执行实体发现及路由机制对各层实体进行分类及分组,对上层实体暴露统一的调用门面,除了起到逐层向上屏蔽分组内部场景复杂度的作用之外,PICASO框架维护的可执行实体路由表也可以作为业务细节逻辑的速查索引。有了这些措施,开发者进行业务逻辑梳理时就可以先通过领域服务维度的路由表定位到目标场景对应的领域服务实例,然后通过其领域能力执行图快速建立起对务流程的全景认知,接着选择执行图中感兴趣的业务节点,通过领域能力维度的路由表快速定位到具体的能力实例,最后到相应的的标准步骤中定位具体的实现细节。这种由粗及细、逐层按索引查找的过程类似于图书馆的管理模式:借阅图书时,我们需要大体推断目标书籍所属的类目,然后通过类目确定书籍所在的书架,在书架上找到目标书籍后再通过其目录快速概览全书,最后通过目录定位到感兴趣的内容,这就是图书馆式代码架构的由来。我们希望这种代码架构能够让业务知识通过系统代码清晰、准确、完整地表达出来并能流畅地传承下去,进而让团队在面对业态更迭时能够更加从容地承接业务方提出的各项诉求;在进行多团队协作时能够拥有足够的底气承担更多的责任;在遇到线上问题时能够更加快速地定位问题并制定解决方案;在接手新系统时也能够快速梳理出业务主线并上手开发,最终实现团队整体研发效率的提升。
(二)聚合与资源库:拒绝魔法逻辑,让代码直接表达业务规则
如果说前文介绍的PICASO框架让新架构拥有了领域驱动设计之形,那么接下来将要介绍的聚合与资源库机制将让新架构真正具备领域驱动设计的灵魂。
长期以来,我们的开发行为中业务设计与代码设计是分离的。接到需求后研发人员会和产品及业务方进行大量的沟通,确认业务流程及各项细节规则,这是业务设计的过程。但是在编码阶段,开发人员又会对业务设计结果重新进行抽象,转化为代码实现方案。而此时传统的三层架构过于简单的层次划分很容易将开发者的大部分注意力引导到数据库设计上,代码设计就变成了对数据库增删改查操作的设计。在这个过程中前期业务设计沉淀的大量领域知识往往会被丢弃,实现出来的代码也失去了对业务规则的直接表达。而缺乏基础模型设计的软件充其量也只是一种机械化的产品,虽能实现功能却无法解释这样操作的原因。更严重的是,如果底层数据库存在表或字段的复用,那么业务规则被直接翻译成库表增删改查操作逻辑之后,代码甚至会表达出与业务规则完全不相符的含义出来。这些代码就成了只有开发者自己才能看懂的魔法逻辑,甚至经过一段时间之后开发者本人也会忘记这些代码背后真正的业务含义......
如果整个程序设计或者其核心部分没有与领域模型相对应,那么该模型就是没有价值的,软件的正确性也值得怀疑。(Eric Evans, Domain-Driven Design: Trackling Complexity in the Heart of Software, 2003)
面向数据库编程的设计思维还容易引导我们最终以事务脚本(Transaction Script)的形式实现业务流程的处理,将业务规则与数据库表操作逻辑糅合到一起,业务实体之间的关系也因此分散和隐藏到了整个工程中不同的接口实现里,这会给数据模型全景认知的建立带来极大的障碍。领域驱动设计思想的祖师爷及布道者Eric Evans曾提到自己项目组的成员曾花费数月时间才梳理出一个完整的数据模型,而在我们自己的记忆里,似乎也没有哪位同学有底气敢宣称自己掌握了完整的数据模型(我们可能清楚数据库中有哪些库表,但是由于底层库表存在不同业务场景复用的情况,导致表中数据对应的业务含义并不统一。这种复用表结构的设计无可厚非,在很多情况下我们甚至都鼓励这种纵向拓展方式,但这也的确是造成我们对数据模型认识模糊和不完整的主要原因)。数据模型全景认知的缺失让开发者很难进行统一的模型顶层设计,在多需求并行推进的开发模式下很容易在不同项目组之间形成信息孤岛,无法实现模型复用与合并,导致数据模型野蛮膨胀,这反过来又进一步加剧了数据模型全景认知的构建难度,从而陷入到恶性循环中无法自拔。除此之外,业务流程中不同的业务环节可能会依赖相同的底层数据,事务脚本式的业务处理逻辑会将这些底层数据的读写操作分散到不同的接口或业务模块的实现中,除了会造成重复编码之外,还有可能在运行时造成重复和碎片化的数据读写操作,进而影响系统性能。
聚合与资源库机制就是专门为解决上述问题而生的。
聚合与资源库机制实现数据模型与业务逻辑分离
聚合(Aggregate) 是领域驱动设计思想中重要概念,它是一组相关对象的集合,这些对象之间关联密切,彼此之间已经按照对象之间的关系以父子属性的方式组装好。每个聚合都有明确的边界(boundary)和一个聚合根(root),其中边界定义了这个聚合中都有哪些对象,而聚合根则是这些对象中的一个特定实体。聚合根是聚合内唯一个允许被外部对象引用的实体,也是聚合中的所有实体的最顶层父级对象,因此通过聚合根可以访问到聚合内所有对象,这会给上层业务规则的实现带来了极大的便利。但是实际业务中聚合内的实体关系通常都十分复杂,常常存在多级嵌套关系。比如广告投放业务中计划聚合内就存在着“计划->单元->创意->子创意->子创意审核记录”这种多达5层的嵌套实体结构。如果每次使用聚合时都需要开发者手动实现聚合内实体的组装逻辑显然会带来大量的重复代码及编码负担,对系统未来的维护来说也将是一场灾难,而资源库就是为了解决这个问题而设计的。
资源库(Repository) 是系统中所有聚合根对象的逻辑集合,当然这并不意味着资源库对象会直接加载和维护系统中所有的聚合根实例,它只是逻辑上的集合。事实上在实现层面业务数据始终保存在数据库等存储介质中,资源库只是定义了针对聚合根或聚合根集合的增删改查操接口,并且维护了底层存储介质中的数据记录与聚合实体对象之间的映射关系以及聚合中各个实体之间的关联组合逻辑。因此开发者可以通过资源库定义的标准接口一键获取到一个组装好的聚合根对象,就好像是从一个集合中取出一个元素那样容易。如下图所示,与传统架构业务层代码在不同业务环节中直接访问数据库的实现方式相比,新架构在上层业务(领域服务)与底层数据库访问层中间插入了一层资源库,上层业务需要获取聚合数据时,不需要自己实现聚合中各个业务实体的查询、映射和组装逻辑,而是通过资源库提供的标准接口直接获取已经组装好的聚合根对象。这种设计将聚合内各个实体的查询、映射及组装逻辑收口屏蔽在资源库内部,让上层业务聚焦在业务规则上,从而实现了数据模型与业务逻辑的分离。这样不仅能够避免在业务逻辑中频繁穿插繁琐的数据查询和组装逻辑,防止在运行时出现重复及碎片化的数据读写操作,资源库集中维护的各个聚合实体的查询、映射和关联逻辑也是一份重要的业务知识,能够辅助开发者快速建立对完整数据模型的全景认知。
聚合与资源库的引入实现了数据模型与上层业务模型的分离
六边形架构让代码从业务中来到业务中去
在上一个小节我们介绍了聚合和资源库的定义及实现准则,这其实有些本末倒置,因为聚合和资源库机制的关键不是如何实现聚合实体的查询或者持久化,而是如何设计一套有价值的聚合(当然,先了解聚合的实现准则对理解聚合的设计准则是有帮助的),而聚合的设计原则正是领域驱动设计思想核心理念的直接体现。
网络上很多介绍DDD思想的文章都是从统一语言、领域、界限上下文等术语和概念开始的,但是由于中英文语境的差异和国内外软件开发生态的不同,这些文章很容易让初学者陷入到各种概念的泥沼中无法自拔。然而那些概念只是领域驱动设计思想的外在表现形式,其背后的思想内核才是我们应该优先掌握的内容。领域驱动设计思想的核心理念就是保持业务、领域模型和代码三者之间的统一,而六边形架构就是为了保障系统能够达成这一目标而设计的,它的核心逻辑在于保护领域模型。
六边形架构实际上是在分层架构的基础上升级而来。领域驱动设计思想作为一种设计的指导思想其实并不会限制使用某种特定的架构,在传统的分层架构上也可以实现领域驱动设计思想的落地。下图中最左侧给出了应用了领域驱动设计思想之后的分层架构,它看上去就是将传统三层架构中的业务层拆分为应用层与领域层。其中领域层负责维护领域模型,所谓的领域模型就是由当前领域内的全部聚合、资源库以及运行在这些聚合实体上领域能力和领域服务构成的;应用层则从业务视角定义了系统应该对外提供多少服务(也就是所谓用例(use case)和用户故事(user story),如果你特别执着于那些术语的话),这些服务接口最终都会调用领域层中的领域服务执行器来实现接口功能;应用层关注的是在系统应该对外提供哪些服务,但是并不关心这些服务的请求来源是RPC调用还是MQ通知,这是用户接口层的职责,它负责根据与调用方达成的约定将应用层接口暴露给不同的接口协议:Http、RPC、MQ、事件通知等等;基础设施层则负责维护系统中使用的众多中间件、工具以及底层存储介质的访问逻辑。
多层架构向六边形架构的演进历程
通过领域层和应用层的抽象,我们让分层架构具备了实施领域驱动设计思想的可能。但是分层架构天然会引导开发者自上而下地以数据流的视角审视系统。而人脑的天性使我们更容易关注流程的始末,而容易轻视流程的中间环节。因此分层架构容易让开发者更多的关注底层数据的存储逻辑,进而再次陷入面向数据库编程的思维,设计出与实际业务脱节的领域模型。当然,我们自然可以通过不断的宣贯和世界观输出来呼吁开发者紧贴业务实际设计代码实体,避免在需求伊始就陷入到底层库表结构中无法自拔,但我们还是希望找到一种能够在架构模式上就凸显领域模型的重要性,引导开发者从业务实际出发设计领域模型的架构,这就是六边形架构诞生的背景。在对六边形架构进行详细介绍之前,我们先回过头来再次审视分层架构中用户接口层和基础设施层的职责差异,这对理解六边形架构的本质十分重要:用户接口层将系统服务暴露成不同的协议接口,因此其内部的代码主要在执行接口参数转换和应用层接口调用的逻辑;在领域服务返回处理结果之后,用户接口层还需要将领域服务返回的响应结果包装成符合对外接口协议的响应对象。而基础设施层主要维护的是底层数据的访问逻辑,似乎与用户接口层的职责千差万别。但是如果我们把数据库看做是一个特殊的外部服务,基础设施层的代码执行逻辑就与用户接口层几乎一致了:基础设施层负责的就是聚合实体与底层存储PO对象之间的转换及存储介质数据传输协议的调用。我们不难发现用户接口层与基础设施层都在针对领域模型做防腐处理,从这个视角看,用户接口层与基础设施层在架构中的地位是相同的。因此,如上图中间位置的架构图所示,在架构模式上我们尝试将用户接口层与基础设施层 “掰” 到一个同一个层级中合为一体,于是我们就能得到一个六边形的对称架构(从上图的呈现方式上看,将用户接口层与基础设施层合并后可能还需要再旋转45°才能呈现出与上图右侧一致的六边形架构)。
六边形架构本质上还是一个分层架构,只是在呈现方式上(注意不是实现方式)将用户接口层与基础设施层合二为一,让他们共同作为防腐层保护位于架构中心的领域模型不被调用方的请求协议以及底层数据库的特殊设计所污染。而人脑天然具备的找中心的特性能够让开发者将更多的注意力放到位于架构中心的领域模型上,暂时忘记底层数据库的存储规则,进而能够紧贴业务实际设计聚合中的各个实体及值对象,让代码直接表达业务规则,最后通过资源库实现聚合实体与底层存储介质PO对象之间的转化。
下图给出了一个广告投放业务中聚合实体与底层库表结构设计的例子,投放系统作为一个业务集成平台需要不断地对接各种垂直业务系统,在广告物料中也需要不断集成各类业务实体,这些数据也需要保存到底层的广告物料数据中。在设计单元聚合时,我们会紧贴业务实际为各类外部关联对象定义含义明确的聚合实体:商品库(ProductCategory
)、抖音账号信息(AweneAccount
)、展示锚点信息(AnchorInfo
)、直播信息(LiveInfo
)等,从而确保领域模型与实际业务的统一。但是在设计底层库表结构时为了避免库表结构膨胀以及表字段稀疏化,我们采用了通用化的存储结构:绑定到广告物料上的不同外部业务对象都保存到同一张外部关联对象表中,该表中的字段采用泛化设计,不与任何一种特定的外部业务对象相绑定,而是通过type
字段确定本条记录对应的外部对象类型以及表中其他字段的实际业务含义,业务层对这些外部业务对象读写操作都要通过资源库集中维护的底层数据记录与领域层聚合实体之间的映射逻辑做转换。需要特别说明的是,在新架构中我们将PICASO框架中的拓展点机制延伸到了基础设施层,引入了模型管理拓展点(Model Manage Extension, MME) 来实现聚合组装主流程与底层数据对象转换逻辑的解耦。在本例中,我们将外部关联对象表对应的PO对象与领域层聚合实体之间的映射逻辑抽离成了一个模型管理拓展点,以外部关联对象类型作为路由键,通过PICASO框架的通用可执行实体发现与路由机制实现具体拓展点实现的自动定位。可以看出资源库的存在不仅让开发者可以聚焦业务实际设计领域模型,让代码直接反映业务实际,同时还让通用化的底层数据存储结构得到更加广泛的应:库表结构的通用化设计虽然能够解决数据库模型膨胀的问题,但是也的确降低了数据模型对业务的表达能力,而且如果让上层业务直接操作这些库表,势必会造成代码逻辑不明、编码繁琐的问题。然而资源库却通过收口底层数据对象与上层业务实体之间的转化逻辑很好地解决了这些问题,让我们在采纳这种通用化的数据存储结构设计时少了很多顾虑。
底层通用化存储结构(K-V模式)在资源库中通过模型管理拓展点被映射为领域模型中业务含义明确的实体对象
在框架实现层面六边形架构与分层架构其实并没有太多的差异,顶多就是在六边形中领域模型仅负责定义资源库接口,而将资源库的实现放到了基础设施层中。在使用了Spring等控制反转容器的项目中,一些宣称采用了分层架构的系统可能已经在无意间实现了六边形架构了。那么六边形架构的意义又是什么呢?我们在前文中曾引用了IEEE对“架构”的定义:组织、组件以及指导思想,然而很多时候我们都忽略了指导思想对软件开发行为的重要影响。一个好的架构一定是包涵人性的,架构思想直接决定了开发者分析业务的世界观和方法论。框架只是辅助工具,基于框架思想对业务进行抽象和设计而产出的代码才是一个软件系统的主要构成部分。即使采用相同的框架,在不同架构思想的引导之下,系统中的业务代码也可能会走向全然不同的迭代路线。而六边形架构与分层架构的差异正体现在二者对开发行为的指导思想上,六边形架构以领域模型为中心,引导开发者始终紧贴业务实际进行模型设计。它与PICASO框架相辅相成,让系统在应对高复杂度业务时依然能保持对业务规则的清晰表达。
声明式数据操作既要规范又要灵活
如前文所述,一个聚合会包含业务子域内的全部实体与值对象,资源库会维护这些业务实体的查询、映射及组装逻辑。但是并不是每一个领域服务都会用到聚合中的全部实体,如果每次获取聚合根时都将聚合内所有实体都查询出来,势必会造成极大性能损耗。然而资源库作为基础设施层中的底层组件,也不可能为每一个领域服务提供专用的聚合查询或者数据持久化接口。为了调和这两者之间的矛盾,我们在资源库实现上采用了声明式数据操作设计。
在《能力编排领域特定语言》章节中我们已经对声明式编程有了比较具象的体会,但声明式编程并不限定于领域特定语言这一种实现形式,在资源库的接口设计上我们采用了一种更加朴素的声明式编程实现方法。如下图所示,我们计划聚合的查询以及创意聚合的更新接口为例来阐述声明式数据操作的设计细节。当需要从资源库中获取聚合根对象时,默认情况下资源库库会自动查询聚合下所有子实体并完成组装,但是开发者可以再提供一个额外的辅助查询参数DomainQuery
,并通过该对象的addSubEntityQuery
方法声明希望获取哪些特定的子实体。默认情况下资源库会根据聚合根对应的主表记录ID做子实体查询,如果开发者希望在子实体查询时使用额外的匹配条件,则可以通过addSubEntityQuery
方法同时声明希望获取的子实体类型以及执行该类型子实体查询时使用的额外匹配参数,这一特性在与聚合根存在一对多的子实体查询场景中会发挥重要作用。上述声明式接口在资源库实现中并不复杂,只需要在执行每个子实体的查询或修改操作之前,判断一下上层调用方是否限定了仅对部分子实体进行操作,如果设置了则进一步判断当前子实体是否在上层调用方指定的子实体范围之内,如果当前子实体业务上允许调用方指定自定义的匹配逻辑,还需要尝试从辅助查询/修改参数中提取调用方为该子实体指定的自定义匹配逻辑。需要特别说明的是,我们不推荐在新架构内部各个模块之间采用任何形式的黑盒调用模式,不管是能力编排还是资源库调用,上层调用方都有责任和义务理清底层模块的内部执行逻辑,确保编排配置或调用参数设置正确。
基于资源库进行声明式聚合数据查询
四、新架构下的业务开发流程速览
业务建模
新架构作为领域驱动设计思想的战术落地框架,能够让其发挥出最大价值的前提是对业务进行良好的领域建模,下图给出了广告投放平台中竞价及合约广告投放服务的业务架构。由于本文的主题聚焦在如何脚踏实地地实现一个基于领域驱动设计思想而设计的系统,因此关于DDD思想战略设计相关的内容本文将不做过多阐述,相关内容我们将在后续《领域驱动设计与PICASO框架》一文中进行详细阐述。需要说明的是,图中的聚合服务层、领域能力层及数据模型层共同构成了广告投放平台和核心领域模型。
竞价及合约广告投放服务分层业务架构
工程结构
下图给出了新架构思想落地时工程结构的最佳实践案例,工程中各module的职责以及与上文中业务分层架构图中各层的对应关系依次为:
rtbad-framework:框架包,承载架构标准规约及分层架构图中基础设施层中的各项基础组件。
rtbad-module/rtbad-support-module:模型包,对应的是分层架构图中的模型层,承载领域模型中各个聚合及聚合实体对象的定义,另外用于实现底层存储介质中持久化数据与领域模型中实体对象映射逻辑的资源库也定义在此类module中。其中support-module定义的是支撑域中的实体对象,该module下会按照业务子域进一步进行子module划分。
rtbad-event:事件包,属于特殊的数据模型层,承载这领域事件对象的定义,新架构底层融合了事件驱动架构(篇幅的关系我们会在其他文章中进行专门地介绍),因此事件体系建设也被纳入到统一建模的工作中。
rtbad-composite/rtbad-support-compoaite:聚合服务包,对应分层架构中的领域能力及聚合服务层,承载领域能力及领域服务执行器的实现,其中support-Composite用于承载支撑域领域能力及领域服务执行器的实现,该module下会按照业务子域进一步进行子module划分。
rtbad-app:部署层,内部分为不同的子module,每一个子module对应一个部署应用,其实现逻辑就是根据应用职责组装底层各个子module,进而实现不同应用下能力及模型共享。
rtbad-api:对外接口SDK包,承载了对外提供的API接口定义。
代码开发流程示例
以下内容以一次计划新建请求为例,通过从流量入口到数据落库的完整请求流程展示使用新架构实现业务需求的全部过程,我们希望通过这个例子让大家建立对新架构的直观感受。
领域服务统一入口
领域服务统一入口(Domain Service Faced)的作用是为HTTP、RPC、MQ等上层不同的请求流量暴露统一的服务入口。他将同一个业务子域内领域服务执行器集中到一起,便于流量介入层调用,同时也可以集中进行方法性能监控、调用量统计、请求日志记录等通用功能。
领域服务门面执行器
领域服务统一入口引用的是各个领域服务门面执行器,它负责从参数中提取业务标识并定位到具体的领域服务实例。如下图所示,所有具体的领域服务实例都继承自领域服务门面执行器,请求到来时领域服务门面先通过generateRouteKey
方法从当前参数中提取出本次请求的业务标识,然后与各个领域服务实例能够支持的业务标识做匹配,从而定位到应该处理本次请求的服务实例。
领域服务实例执行器
能力执行图的作用是定义业务执行流程,框架提供了丰富的API及大量的语法糖和默认规则,配合链式调用的风格,在支持灵活编排的同时减少了开发者的负担。
领域能力门面执行器
负责从参数中提取场景标识并定位具体的领域能力实例。
领域能力实例执行器
能力内主要负责实现具体的业务规则,并对聚合根中相关属性进行设置/修改,一个领域服务编排的所有领域能力执行完成之后,就能获取一个完整的、全新的聚合根对象。
拓展点
拓展点的作用是作为任意维度的差异点补充分离工具。需要注意的是在新架构中拓展点不是唯一的差异分离工具,在通用对象发现及路由机制下,领域服务、领域能力和拓展点都在不同维度上起着差异点分离的作用。相对与前两者拓展点更加灵活,通常用来承接领域服务及能力自身路由维度之外的逻辑差异。比如出价设置能力已经按照出价方式做了能力维度的差异分离,但是在相同的出价方式下,京准通与流量货币化还存在一些细微的逻辑差异,那么这个时候就可以在该能力实例中通过拓展点来补充实现平台维度的差异逻辑分离。
上面的例子是在计划名称设置能力中获取不同产品线计划名称长度上下限配置的拓展点,计划名称设置能力自身按照产品线类型进行业务模式路由,但是站内计划名称设置主要业务逻辑基本一致,仅在部分校验逻辑上不同产品线有各自的要求,此时就可以使用一个名称设置能力实例服务所有的站内产品线,定义名称设置主体业务规则,而把不同产品线的细微差异抽象成一个拓展点接口。
资源库
上层业务通过声明式接口实现聚合实体读写操作。
五、结语
很多同学对业务开发一直存在一种偏见,认为业务开发很简单,甚至有业务开发同学自己也时常调侃自己是CRUD工程师,认为自己的工作没什么技术含量。但其实业务开发一点都不简单,只是过去我们一直把它做简单了。如今业务形态复杂多变,商机转瞬即逝,如何在快速变化着的复杂业务需求中维持系统健康、稳定、持续迭代,要做到这一点的难度其实一点都不比底层技术差。程序员应该是一门充满学术性与创造性的职业,我们唯有坚守初心,不断夯实自己的技术功底,沉淀提升抽象与建模能力,培养自己的系统化思维,不断学习精进,追求极致编码,这才是我们无法被AI替代的核心竞争力与价值所在。
有同学可能会关注本文介绍PICASO框架未来是否可以对外共享,关于这个问题我们的答案是肯定的。在PICASO框架开发之初我们的野心就没有局限在京东广告投放平台这一个业务场景上,而是希望它可以走出广告部,甚至走出京东,接受全社会开发者的检验,成为一个被业界认可的复杂B端业务通用解决方案。然而作为一个一线业务团队,快速支持业务方需求是我们的首要职责。尽管我们在进行PICASO底层框架开发时尽力维持与具体业务分离的开发原则,但是在需求排期比较紧张的时候,为了快速支持业务需求的开发,还是存在将与广告投放业务相关的逻辑耦合到了PICASO框架底层源码中的情况。如果大家有兴趣阅读或者试玩PICASO的源码,请联系笔者为您开放一个示例版本的框架源码权限,该版本框架的功能与笔者负责的线上系统使用的框架功能完全相同,只是去除了广告投放业务相关的逻辑。您可以在该版本源码上执行任意的功能及性能测试,但是在我们对外发布正式的共享版本之前,我们并不建议您直接将该示例版本的源码应用到线上系统。目前框架功能已趋于稳定,我们也将PICASO框架的开源化改造提上日程,也欢迎感兴趣或者有开源社区维护经验的同学一起交流共建。