ToB类产品有非常丰富的变更需求,而大多数多样化的需求背后,反应到系统层面都是业务核心实体的变更,因此,一套可定制化的业务系统对于这类场景而言非常有帮助,不仅可以大大缩短研发过程,同时更有利于整个系统的扩展。我将探索此类可定制化业务系统的架构,试图找到一些共性,提供一种架构思路。本文是系列文章第一篇,本文将聊一聊字段可定制化。
背景
我在长期的工作中,需要不断的面对业务的变更所带来的各种细碎的需求。其中很多需求只是改动一些细节,但是,从研发层面,却要经历需求评审->开发->测试->发布等一系列过程,而在我看来,这些处理费时费力,非常浪费。有没有一种可能,类似这样的变更,不需要更改代码,只需要通过在线的配置变更,就可以完成细节功能的变更呢?
其实,这一想法也正是需求方在想的,他们也希望系统能够根据需求主动进行配置,而无需每次都需要进行漫长的研发流程。对于业务方来说,迭代发版太漫长了,有时候根本等不到发版周期就想要上某一个更改。同时,有时候,研发同学实现的需求还不一定准确,如果能将一些可定制的能力交给需求方自己去定制,那么实现的也更准确。
可定制化业务系统与低代码平台的区别
看上去,上面所表述的可定制化与低代码平台的效果非常像,都是可以让非开发人员可以主动参与到产品的创建过程中。然而,它们存在着本质的区别。
- 产物不同:低代码平台的产物是具体的应用,同时包含了用于存储数据的数据结构;可定制化的产物是具体的元数据,对于应用来说,是用这些元数据去实现效果
- 素材不同:低代码平台的素材是有平台提供的组件,而可定制化围绕业务,需要的组件得根据业务需求进行定制
- 资源不同:低代码平台的用户不需要自己设计数据库,只把平台产品当作设计工具使用;可定制化是在原有数据库的基础上进行扩展定制,其数据库结构虽有改动,但大部分还是继承业务本身的特性
- 侵入程度不同:低代码平台要求你把整个功能托管在平台上,从底层到应用都是由平台提供;可定制化则在原来的系统基础上改造,或者作为系统的一部分参与完成整个业务
总而言之,低代码平台是相对而言独立的完整的设计工具,先有低代码,再有业务功能;而可定制化是在原有系统上提供可扩展的灵活的元数据编辑能力,先有业务系统,再有可定制化能力。
字段可定制化
简单讲,字段可定制化是指可以通过配置而非写代码的形式,实现字段元数据的变更。这些变更往往对应两种需求,一种是希望展示效果发生变化,例如改变字段的名称,改变数值的展示格式等,另一种是希望变更字段关联等业务逻辑,例如该字段原本由另外3个字段计算后得到结果,现在需要变更为由4个字段计算得到结果,再例如该字段以前在另外一个字段值大于10时必填,现在要变更为大于5就必填,等等。
我想,做业务系统的工程师们在自己的日常中,会经常听到希望自己的系统具备以上的能力。但是,自己下来思考之后,又会发现,这会牵扯出很多问题。有没有一种方案,可以在尽最大可能维持当前系统的设计上,扩展出字段可自定义配置的能力呢?接下来,我将试图向你展示一种可能的设计。
字段可定制化分析
我们需要去思考,什么样的字段是可以定制化的。在业务系统中,有些字段具有特殊逻辑,在业务中,是不可更改的,而有些字段则仅仅是为了记录一个数据,可随意更改。因此,我们可以把字段分为3类:
基于这3类字段,我们可以想象出当我们在进行字段配置时,可能看到的界面场景:
所谓预设,也就是系统先帮你用上,你不需要的话,可以删除。
字段可定制的本质,实质上是让我们可以主动去定制业务对象(上图中的“维度”)。业务对象及当前业务事件发生的主体,及其参与者。和低代码那种从零开始自己搭建不同,我们定制业务对象,一般可以通过脚本把一些基础的固定的内容先倒入到数据库中,即初始化阶段。只有到后期需要进行细节变更的时候,管理员可到后台主动变更字段属性。
这种强烈的业务属性还会带来另外一种情况,即如果一个业务中存在一个字段,那么必然存在另外一个字段。这两个字段可能是一因一果,也可能是合因一果中的因子,总之,当一个字段出现的时候,意味着另外一个字段必须在当前这个业务对象中。
默认状态下,通过一个控制来决定哪些属性是默认给出用来编辑的,如果点击“显示所有选项”则把剩余的属性也展示出来。
无论是添加,还是编辑字段,都是对字段属性的编辑。字段属性是用以描述这个字段的元数据,可以和字段值剖离,也可以和字段值相互影响(例如通过元数据决定字段的值如何计算得到)。因此,实际上,字段的定制化的核心,是提供可覆盖业务需求的字段属性的定制化。怎么才能覆盖业务需求呢?我们慢慢分析。
首先,和字段一样,属性也分3种:固定属性、预设属性、自定义属性。它们在行为上和字段的种类的行为相似。
固定属性必须被设定值。预设属性可以留空不设置值,并且在默认状态下收缩起来,点击上图中的“显示所有选项”时在展示出来用于设置。自定义属性默认情况下(第一次创建时)不存在,需要手动添加属性用于设置。
在一些场景下,某些属性是被绑定在一起的。
上图中,当“数据类型”被选中为数字时,“格式”属性被立马展示出来,它们本质上是两个属性,类型是一个属性,格式是一个属性,只是它们强关联而已。并且不同的数据类型,其格式的配置完全不同。例如日期需要配置日期/时间格式等。
属性可以分为3个大类:
分类 | 定义 | 举例 |
---|---|---|
业务性质 | 描述和业务本身的定义,业务逻辑相关的属性 | 字段类型、校验器 |
交互性质 | 描述在界面上用于控制该字段的展示逻辑或交互逻辑的属性 | 字段在表单中是否需要展示(在什么情况下需要展示) |
技术性质 | 描述在数据提交时(前后端交互时)数据应该以什么方式提交到后端接口 | 名为gp_name的字段实际上对应后端接口的gp.gp_name,其中gp是一个对象 |
属性可以分为9个子类:
属性子类 | 定义 | 举例 |
---|---|---|
值相关 | 该属性在前端运行时,使用该属性作为状态 | value, compute, getter |
校验器相关 | 在验证该字段值时使用 | required, validators |
类型相关 | 控制该字段的数据类型时使用 | type |
存储相关 | 导出数据时,该字段以什么形式进行转化 | save, create |
提交相关 | 提交数据到后端接口时,该字段要怎么处理 | map, flat |
逻辑相关 | 当前字段在当前环境下是否形成了某种状态 | required, readonly, hidden |
监听相关 | 当前字段的值发生变化时被调用 | watch, catch |
衍生资源相关 | 当前字段依赖另外一个字段,那么在对象中就必须同时存在这两个字段,否则就会出错 | deps |
自定义 | 其他自己自定义的属性 |
这些类型在具体设计时不一定都会用得到,即使在同一系统中,不同字段可能也各不相同。
相同的属性,在被不同字段使用时,也有权限的区别,例如在字段A中readonly这个字段是可以被定制化的,但是在字段B中readonly这个属性是规定死的。这需要我们在设计时,让我们的系统可以灵活的保持这种权限关系。
属性值必须是支持动态计算的,例如required属性可能由对象的另外一个字段的值来决定,此时的required在进行定制时,一定时一个表达式,而非固定死的值。在很多定制场景下,甚至设计者不会想到还有这个设计,因为他们没有接触过同一字段的同一属性在不同情况下内容不同。
属性值需要在不同的条件下动态的给出结果。
上图中,“提示语”这个属性需要去“报价单”这个维度中找,查找的条件可以自己配置,找到的是多个报价单对象,取值只取其中一个,且取的是报简单的“报价时间”这个字段的值,取值条件是取(筛选结果中的)“最大值”。
动态取属性值的设计意味着你不能在设计时把属性值直接当作一个固定值对待而设计死,你需要创建一套表达式规则,通过表达式来动态读取需要的值。
提到这里,字段的值也可以是动态的,我们称为“关联类型”。例如,“订单”的“商品”字段实际上是关联“商品”对象的ID,而且它们之间的关系是1:n(一对多)关系。这个设计需要在数据类型这个固定属性处实现。
最后一种情况是,在我们的产品中,同一个属性,可能在不同情况下,需要采用不同的值。例如,“消费额”这个字段的“格式”属性,在个人详情页需要使用“10,000,000.00”这种格式,而在记录页面为了省空间又需要使用“10mn”这种缩略形式。这里引入一个叫“方案”的概念,这也就意味着,同一个字段,其属性有多套方案。这该怎么设计呢?
也就说,同一个字段的同一属性,在不同情况下还有的选择。不过,是否启用方案应该是可选的,如果某些属性没必要启用方案,那么就没必要勉强去设定多套方案。
最后,每一个字段“数据类型”这个属性起着至关重要的作用。一个数据类型,带来的是整个字段在存储、配置交互上的巨大差别。在上面的阐述中,还有一种情况是没有提到的,就是我们一个字段,可能关联另外一个对象,在数据库中以外键表示。比如,一个订单对象的付款字段可能将链接到一个具有自己实体的付款对象上。对于这类数据类型,我们要怎么表示呢?
在stripe这款headless cms中提供了一种链接方式,回到本文,我们可以内置一种叫“关联”的数据类型,该数据类型将由插件来实现,比如某个插件实现的是关联到另外一个业务对象,而另一个插件关联到另外一套系统上(例如多语言配置系统)。等下你将看到具体到存储形式,所以,只要插件按照特定的协议决定保存的数据结构,就可以被使用到其中。另外,“关联”还存在着关联关系的具体表示,例如一对一、一对多等,这个表示实际上是和“关联”这个数据类型强相关的另外一个属性来记录。
字段可定制化表结构
为了支撑上面的分析中提到的这些点,我们必须在设计时尽可能的让表可以灵活扩展。
这些表与实际的业务表分开,与业务表没有本质上的关联,如果不使用字段定制化的能力,这些表可以从业务系统中删除而不影响原有业务数据。
dimensions表即维度表,它的作用是让我们方便的快速找到一个维度都有哪些字段,通过维度表fields字段可以从fields表中读取出这些字段。不过需要强调的是,dimensions维度表中的一个维度并不等于一个数据库表的引用,一个维度实际上可以囊括多张表中的多个字段。fields中的key建议使用table.column的方式命名,这也就意味着,维度中引用的字段table部分可以不同。
fields是字段表,它指明了字段可定制化系统中的字段(key)和原始业务表字段(table column)之间的关系。fields这个表里面的key是原始业务表字段的别称,在字段可定制化系统中使用。name是字段的展示名。type是字段的数据类型,用以控制原始字段数据类型和实际需要的数据类型不一致时的调度。policy用以表示当前这个字段是固定、预设、自定义字段。deps则是前文提到的,一个字段可能必须依赖其他字段,如果定制化系统中deps里有值,意味着这套字段是绑定在一起的。
attributes是整个字段可定制化的核心表,虽然它的字段比较少。一个字段有哪些属性被设置了值,全部在attributes表中(不包含方案部分),一条attribute就是其中一个属性。从上文的图中,你可以看到,不同的属性,在进行配置的时候,交互非常不同,可以说是千差万别。
这时,我们看attribute_configs表,这个表是用于打开属性编辑弹窗时,展示出来可配置的属性的列表,然后从attributes表和schemes表中拉取数据进行填充。
当我们打开属性弹窗时,首先从attribute_configs中读取所有备选属性,其中根据policy进行分类,0:固定属性立即展示在界面上,预设属性1:立即展示在界面上,2:可以通过“更多”展开,3:自定义属性不会出现(在添加自定义属性时,可通过下拉复用)。type属性决定了当前这个属性的基础交互类型,比如字符串、数字、下拉列表等等,这个type是决定属性的交互类型,而非字段的类型,这里需要避免混淆。required代表这个属性必须填写,不能留空(固定属不能留空,因此,可以认为required只针对预设属性)。needs表示当前这个属性依赖其他属性,如果被依赖的属性不存在,会造成错误。component这个字段是交互的关键,它是一个复杂的结构,里面包含了这个属性在界面上的配置,也就是说,当前这个属性在配置界面上怎么交互完全由component决定。同时,component中的内容决定(type字段也有影响,因为基础type可能不需要component)了attributes表中value字段的值,attributes.value这个字段和schemes.value字段也是一个复杂结构(这两个字段结构相同)。为什么是复杂结构呢?因为每一个component输出的结果不同,比如有的输出的是固定的数值,有的输出的是动态表达式,有的输出的是字符串,甚至输出JSON数组,所以,value必须是一个字符串类型,实质上是一个JSON在数据库中的表达形式。它的结构可以设计为复合json-schema的结构。
scheme_configs归档了有哪些方案可以使用。其中一个需要理解的地方在于attributes字段,它代表当前这个方案仅支持这些属性。在交互上,仅当处于attributes中的属性被选中时,右侧的方案列表中才会出现这个方案。
schemes这个表实际上和attributes这个表是平行的,或者你可以认为它是attributes表的补充。如果没有“方案”这个东西,那么不需要它,如果需要“方案”,那么在读取属性值时,需要考虑从对应的方案中读取,如果没有对应方案,就退回到attributes中的值。为了查询方便,schemes中冗余了name字段,这样在查的时候就不需要去读scheme_configs表。
让我们回到具体的某个业务场景下。现在,我们需要展示公司详情,展示时,需要根据字段定制的结果展示这些信息。此时,我们后端在处理时大致流程如下:
基于这一设计,我们可以在完全不该动原有业务,在原有业务之外再建立一套系统实现可定制化的能力。
字段可定制化微服务化架构
要支撑上述设计,我们需要一个可以独立于原有业务的系统,同时又有一定扩展性的功能。我们采用微服务架构将字段的定制化设计为独立于原有业务的服务,再改造原有业务中关于数据读取的逻辑来配合微服务获取最终的结果。
字段定制服务作为基础服务中的一项,保持自己的数据,通过接口形式与业务服务交互。一个业务服务只针对一个业务提供服务,它会同时调度多个基础服务,完成必要的各种功能,其中包含字段定制服务。在请求中,按照上述请求逻辑获取数据,并返回给请求方。
结语
本文详细阐述了业务系统可定制化中的字段定制化的设计思路和方法。业务系统与那些通用的系统之间有着明确的区别,即业务系统有非常明确的领域边界,业务内某些逻辑是固定的,这些固定的,或领域特有的,就没有必要做成通用化的东西,而应该提炼成独立的领域组件,这样才能避免无止尽的自定义搭建,同时也可以避免通用自定义搭建系统无法实现某些具体逻辑的问题。
虽然本文展示了自己的架构设计,但是,这个设计是建立在整套系统通盘考虑都基于微服务搭建,但在真实场景中,我们的业务系统往往是从最初的单体应用发展而来的,原有的设计不一定可以服务化,而如果推翻重来,风险也很大。因此,我们上面的这套设计,还可以兼容已有的系统,或者说,现有的这套正在运行的单体,对于定制化体系,是可有可无的,如果想花功夫接入定制化系统,那么可以在原来的系统上做好粘合,就可以逐渐迁移,避免高成本的一次性重构带来的风险。
业务系统可定制化还包括流程可定制化、表单可定制化、界面可定制化,关注本公众号,这些可定制化设计将在后面的文章中慢慢聊。