前段时间,我写过一篇文章,侧重从产品角度去谈低代码平台的一些能做和适合做的事情,以及做这些事情的方式。

在本篇中,会尝试从几个大方向出发,简单谈谈如何从技术视角看待模型驱动的低代码平台的整体架构,以及相关的部分选型。

侧重点

如果说,低代码平台与通常的技术开发框架有什么差异,那就是它们对于代码和配置的侧重不同。

  • 代码优先
  • 配置优先

很显然,如果表达能力不损失,那么,低代码平台是配置优先的,并且其中的配置表达能力还需要比较强大才行。

所以,我们在技术选型的时候,要时刻牢记,这与我们通常写代码用的技术框架不同,整体不是为了写代码写得舒服,而是为了打造一套能让不同技术能力的人合理协作的机制。

还是用 Excel 来类比:有的人倾向于组合现有的公式;有的人则无视这些东西,从无到有自己写一些代码实现所需功能。前者,是我们要服务的主要对象。

类型

类型体系是一种非常有趣的东西,它可以用来表达结构的组合与变换,从而初步验证变换的安全性。

基于元数据的类型体系

在低代码平台中,我们可以借助类型体系,来实现业务实体的结构与关联表达,并且为规则的扩展提供一定程度的便利性,这是一种很有用的工具,也可能是整个模型驱动的低代码平台的底层方法论的核心部分。

通常,我们会在某种编程语言中,借助这种语言提供的能力来表达原子与复合类型。Web 前端最常用的类型描述体系是 TypeScript,但是需要注意到,TypeScript 能够帮助我们验证的东西,是在编译期确定的,而在低代码平台中,有很多东西是编译期不确定,要在运行时检验。

比如说:

用户创造了“学校”模型,并且为其动态添加了一些字段。

整个这个过程,是完全在运行期完成的,如果我们想要为它添加类型验证,就需要从另外一些角度来解决问题。

领域模型驱动的低代码平台,天然拥有实体的元数据,并且可以依托它,传递到运行期的每个角落,相当于建立了一套基于 Schema 的类型机制。如果能把这套类型机制显式表达出来,会同时对平台和低代码的开发者有好处。

低代码平台的运行期,实际上相当于是常规软件开发流程的开发期,所以,我们要在这个层面提供类型支持,又因为整个这样的实现跨越了多端,贯穿从服务端到多个客户端的完整流程,其中至少有一个环节经历过序列化和反序列化过程,所以,这个验证过程需要能够同时确保数据的合法性。

这样一来,这套类型系统的职责就是贯穿数据的生命周期了。

类型运算

在已经具备的类型结构上,可以扩展出一套校验机制来。比如,我们知道类型的 Schema 是怎样的描述,就可以生成对应的校验函数来验证对应数据的合法性。

甚至因为存在类型的组合与变换关系,可以动态生成复合类型的详细信息,并且以此作为更复杂数据结构的校验依据。

需要注意的是,这里的组合与变换,已经不是 TypeScript 里面的那种了,这是平台需要实现的能力,即使平台本身不使用 TypeScript 开发,也无损于这种变换与验证能力。

编码提示

同理,基于已有的类型结构,可以在扩展的动作与方法上,提供一些额外的编程便利。不同语言之间的类型结构,是存在一定程度的转化性的,我们基于描述构建的这套类型结构,可以很方便映射到主流编程语言中。

存储

存储是整个系统可用性的一个关键组成部分。

从编程框架的角度,针对结构化数据存储,出现过几种不同维度的抽象:

  • 面向数据库连接:JDBC
  • 面向实体:ORM

前者主要侧重于屏蔽不同数据库之间的差异,一般还要辅以特定的编程语言或者框架所约定的编程方式,比如 Java 体系中的基类、抽象类、实现类等等一套机制。

这种方式暴露的方式比较底层,控制能力强大,没有用这种方式实现不出的需求,但是在低代码平台中,大部分情况下抽象层次过低了。

而广义的 ORM 则是一个比较适中的抽象层,它首先提供了实体视角,可以面向领域模型中的物理层模型去组织数据的存取。

其次,底层的存储未必就一定要是本系统内部的结构化存储,完全可以基于外系统提供的读写 API,映射出一个虚拟的实体,此后,可以把这个虚拟实体当作真实实体一样进行关联或者组合操作。

所以从这个角度,我们可以设计一套分布式 ORM,来抽象整个广义的存储层。ORM 中的实体定义作为贯穿整个系统的元数据描述,可以与类型系统结合,贯穿整个系统的始终。

逻辑

低代码平台最难以“无代码化”,或者可以说,唯一无法全部配置化的就是业务逻辑了,而且很多时候不是无法,而是不适合。

比如说,我们通常会把比较大粒度的逻辑设计为流程,然后用序列或者状态机流程图的方式去编排,但是很难把更细粒度的逻辑也用这种方式编排,因为性价比太低了,徒增理解成本。

但是需要认识到,逻辑也是可以分类的,比如,从触发方式上,可以分为:

  • 主动
  • 被动

主动的逻辑,通常可以组织为某种服务,供某些主动调用方使用,或者由定时器、生命周期触发。被动的逻辑,通常承担的是拦截、验证等职能,一般可以归类为规则。

视图

在一般的应用系统中,视图层是抽象程度最低,而在研发流程中耗时又最多的。

从最基础的视角出发,有没有视图层,提供精致或者简陋的视图层,都不会十分影响一般业务的实质。

基础组成

视图层的基础组成部分包括如下:

  • 原子化交互
  • 布局与编排
  • 状态管理
  • 数据请求

除了原子化交互,是一种可以隔离在外的东西,其他部分都具有一个共同特征:需要使用到实体结构,而这种实体结构,正是在后端存储部分的元数据描述之一,如果我们能让这三块内容尽量复用元数据,则有可能最大可能地降低所需额外编写的代码量。

从这个角度,我们可以从元数据结构为中心,重新组织我们的前端组件体系,以及典型的状态结构场景,并且围绕它们,去解释元数据,生成不同形态的交互系统。

从这个角度出发,再反过来看待之前的几个问题:

最没有争议的是数据请求,它可以表达为基于元数据描述的请求,在下一节详细叙述。

布局和编排

这里通常会存在几个流派:

  • JSX,约束少,人工可读性高,解析难度高
  • 某种模板语言,约束多,人工可读性中上,解析难度中低
  • JSON,约束多,人工可读性低,解析难度低

需要注意的是,在低代码平台中,通常会附带额外的视图可视化搭建工具,因此:

  • JSX 并不是最合适的承载视图编排的工具,主要因为解析难度过高,而自身的编程友好,在低代码平台中是次要因素
  • 模板的人工和机器处理能力都适中,既可以作为可视化编辑的底层存储结构,也可以手工编写
  • JSON 可以比较容易支撑可视化编辑,但是如果熟练工想要手工编写,难度是很高的

所以在这里,很主流的选择是某种模板语法,至于这种模板是基于标签的,还是基于形如 Markdown 这样的结构表达,这是次要的,并不是很重要,两者基本可以视为等价。

状态管理

状态管理的需求可以由内置的典型状态结构,外加可扩展的控制逻辑去支撑。在这个地方,我们可以来看一下近两年来争议很大的 Redux,它的实质是什么呢?

Redux 的实质是:将自身作为一种数据的持有者,并且提供了迭代器模式,让外层可以提供映射器,来处理每次的数据变更。通常在我们手写代码进行编程的时候,这种模式是比较复杂的,约束很多,格式代码也多,但从另外一个角度,它是一种很好的用来承载代码操作配置化的表达方式。

约束多,对编程不利,一般对于配置化都会比较有利。可以以这样的状态结构为蓝本,提供一些内置的动作,然后再提供额外的动作扩展能力,这样就可以很容易实现状态与逻辑的扩展了。

跨端

视图需要解决的另外一个问题是跨端。如果意识到,在我们这个体系下,并不需要为每种组件都寻求相同的跨端表达,而是只要语义等价就可以,那完全可以为各端单独设计原子交互,然后用相同的编排层去解决问题。

唯一需要考虑的是,引擎层在各端与视图层的通信或集成方式。可以尝试在 PC Web 端把逻辑引擎迁移到 Worker 中,交互层使用类似本地 RPC 的通信协议与引擎传输数据,在其他端使用类似的方式,这样,各端的交互都是本地化的,但是逻辑引擎共用一套。

接口

注意到我们把接口放在这么靠后的位置来讨论,因为如果不在跨系统集成语义下,接口其实是个不值得过于关注的部分,因为它是一种系统内部行为。

从视图角度看,由于平台提供的能力,导致它调用的是一种封装过的 RPC 服务,至于其内部是如何传输的,并不十分重要。

但是需要考虑到一点,因为我们之前的考量,把视图整体配置化了,在视图层中,复用了“编排”,并且使其贯穿到“状态结构”与“读写”,因此,接口层必须能够灵活相应视图层的各种灵活结构调用,例如:

  • Partial
  • Array>

以及更复杂的它们的组合。在当前,最成熟的可以响应这类请求结构的方案就是 GraphQL,它提供的两个能力恰好符合我们的需求:

  • 能够把层次化的请求,打平到实体维度的原子化读写接口
  • 能够在单个原子化接口的读写结构上做裁剪

因此,在这里选用 GraphQL 是非常合适的,并且它还便于实现更加复杂而强大的能力,因为下层实体关系的图状结构被凸显了,可以在这一层去做一些权限之类的编排定义。

从这个角度看,我们又可以发现,在整个体系中,前端才是真正的 ORM 的消费者,数据的生命周期一直贯穿到客户端,因此这其中会涉及很有意思的思考,在我之前的某些文章有过比较详细的阐述。

另外一个角度,也可以给接口层提供多套 API 出口,以适配不同的跨系统集成方式。

小结

作为一篇概要性的论述,本文只是简单提及了低代码平台开发过程中的一些主要方面的技术选型,细节内容是非常庞杂的,足够写几十篇来详细阐述,此处空间太小,不一一展开。

总的原则,还是要以可组合性、配置化为最高出发点,在此基础上首先构建出可运行的技术框架,然后再做上层的产品包装。

后记:上次被兔子同学说,我的技术观点和选型方式一贯比较奇怪,比如说:

  • 倾向于模板而不是 JSX
  • 认为视图层的类型重要性不高

主要原因是我的视角站在低代码平台这种产品形态上,考虑的一种前提是要让元数据表达的类型成为整个应用的主题,侧重于各种编排能力,因此写这么一篇简单提一下一些粗略的思考点。