写点什么

Nop 平台为什么是一个独一无二的开源软件开发平台

作者:canonical
  • 2023-06-09
    北京
  • 本文字数:6093 字

    阅读完需:约 20 分钟

Nop 平台与其他开源软件开发平台相比,其最本质的区别在于 Nop 平台是从第一性的数学原理出发,基于严密的数学推导逐步得到各个层面的详细设计。它的各个组成部分具有一种内在的数学意义上的一致性。这直接导致 Nop 平台的实现相比于其他平台代码要短小精悍得多,而且在灵活性和可扩展性方面也达到了所有已知的公开技术都无法达到的高度,可以实现系统级的粗粒度软件复用。而主流的技术主要基于组件组装的思想进行设计,其理论基础已经决定了整体软件的复用度存在上限。


Nop 平台遵循了可逆计算理论所制定的技术战略


 App = Delta x-extends Generator<DSL>
复制代码


为了落实这一技术战略,Nop 平台使用了如下具体的战术手段。

一. DSL 森林

在 Nop 平台中,能使用 DSL(Domain Specific Language)描述的逻辑一定会使用 DSL 来描述。这与主流框架的设计思想是完全不同的。比如说,


  1. Hibernate 中引入数据库方言对象需要指定一个 Dialect 类,而在 Nop 平台中我们会增加一个 dialect.xml 模型文件

  2. Hibernate 中定义对象与数据库表之间的映射关系主要使用代码中的 JPA 注解(最新的 Hibernate6.0 已经取消 hbm 支持),而在 Nop 平台中我们使用 orm.xml 模型文件

  3. SpringBoot 通过注解来实现复杂条件装配,而在 Nop 平台中为 Spring1.0 语法的 beans.xml 增加了条件判断扩展,可以在 beans.xml 模型文件中实现类似 SpringBoot 的条件装配功能。

  4. 解析 Excel 模型文件一般要么像 EasyExcel 那样只支持简单结构,要么靠程序员手工编写针对某个固定格式的解析代码,而 Nop 平台中我们可以通过 imp 模型来描述存储在 Excel 文件中的复杂领域对象结构,自动实现解析,并自动实现 Excel 生成。


图灵奖得主 Michael Stonebraker 在批判 MapReduce 技术时曾指出,数据库领域这四十年来中学到了三条重要的经验:


  1. Schemas are good.

  2. Separation of the schema from the application is good.

  3. High-level access languages are good.


如果我们想要将上述三条经验推广到更广泛的编程领域,那么我们必然需要创建自定义的高层语言,也就是所谓的 DSL。DSL 提高了信息表达密度,同时也提升了业务逻辑的可分析性和可迁移性。我们可以分析描述式的 DSL 结构反向提取出大量信息,并且将它迁移到新的模型中。例如百度的 AMIS 框架在宣传时总是强调 AMIS 的页面描述长期保持稳定,而底层的实现框架已经升级换代了很多次。我们甚至可以编写一个转换器,将 AMIS 的描述转换到某种我们自己编写的 Vue 组件框架上。


在可逆计算理论的概念框架下,我们需要强调信息表达的可逆性,即通过某种形式表达的信息应该可以被逆向抽取,转换为其他形式。而不是像今天的软件一样,信息被固化在具体的语言、框架中,升级底层框架是一件成本高昂的奢侈行为。


传统上 Schema 只是用于约束某种数据对象,但当我们系统化的使用 DSL 来解决问题时,我们所面对的不再是单一的某一个 DSL,而是众多 DSL 所组成的 DSL 森林,在这种情况下,我们有必要为 DSL 引入独立的 Schema 定义,也就是所谓的元模型:描述模型的模型。

二. 统一的元元模型

主流框架的设计可以说是各自为战,比如工作流引擎、报表引擎、规则引擎、GraphQL 引擎、IoC 引擎、ORM 引擎等等,各自编写自己的模型解析和序列化代码,各自独自编写自己的格式校验。如果需要可视化设计工具,则每个框架都自己独自编写一套。一般情况下,除非出自大公司或者成为某个商业产品的一部分,我们很难看到一个框架具有对应的 IDE 插件,具有良好可用的可视化设计工具。


如果所有的模型底层具有严格定义的内在一致性,我们就可以通过逻辑推导自动实现大量功能而无需针对每一种模型单独编写。而统一的元元模型是实现内在一致性的关键手段。


  1. 模型(元数据)是描述数据的数据

  2. 元模型是描述模型的模型(数据)

  3. 元元模型是描述元模型的模型(数据)


举例来说,


  1. ORM 模型文件app.orm.xml定义了一个数据库存储模型

  2. ORM 模型的结构由它的元模型orm.xdef来描述

  3. 在 Nop 平台中,所有的模型都采用统一的 XDef 元模型语言来描述,而 XDef 元模型语言的定义xdef.xdef就是所谓的元元模型。有趣的是,XDef 由 XDef 自己来定义,所以我们不再需要元元元模型。


Nop 平台根据 XDef 元模型自动得到模型解析器、验证器等,通过统一的 IDE 插件实现代码提示、链接跳转以及断点单步调试等功能。更进一步,Nop 平台可以根据 XDef 元模型描述以及扩展的 Meta 描述,自动为模型文件生成可视化设计器。借助于公共的元模型以及内嵌的 Xpl 模板语言,我们可以实现多个 DSL 之间的无缝嵌入。比如在工作流引擎中嵌入规则引擎,在报表引擎中嵌入 ETL 引擎等。此外,统一元元模型使得复用元模型成为可能,比如designer模型view模型都引用了公共的 form 模型。复用元模型可以极大的提升系统内部语义的一致性,从根源上减少概念冲突,提升系统的复用性。


Nop 平台所提供的是所谓的领域语言工作台(DSL Workbench)的能力。我们可以利用 Nop 平台来快速的开发新的 DSL 或者扩展已有的 DSL,然后再使用 DSL 来开发具体的业务逻辑。


Nop 平台作为一个低代码平台,它自身的开发也是采用的低代码的开发方式。一个显著的结果是,Nop 平台中手工编写的代码量相比于传统框架的代码量大为下降。以 Hibernate 为例,它具有至少 30 万行以上的代码量,却长期存在着不支持在 From 子句中使用子查询,不支持关联属性之外的表连接、难以优化延迟属性加载等问题。NopORM 引擎实现了 Hibernate+MyBatis 的所有核心功能,可以使用大多数 SQL 关联语法,支持 With 子句、Limit 子句等,同时增加了逻辑删除、多租户、分库分表、字段加解密、字段修改历史跟踪等应用层常用的功能,支持异步调用模式,支持类似 GraphQL 的批量加载优化,支持根据 Excel 模型直接生成 GraphQL 服务等。实现所有这些功能,NopORM 中手写的有效代码量只有 1 万行左右。类似的,在 Nop 平台中我们通过 4000 行左右的代码实现了支持条件装配的 NopIoC 容器,通过 3000 行左右的代码实现了支持灰度发布的分布式 RPC 框架,通过 3000 代码实现了采用 Excel 作为设计器的中国式报表引擎 NopReport 等。具体介绍参见以下文章:


三. 差量化

在软件开发中,所谓的可扩展性指的是在不需要修改原始代码的情况下,通过添加额外的代码或差异信息,可以满足新的需求或实现新的功能。如果在完全抽象的数学层面去理解软件开发中的扩展机制,我们可以认为它对应于如下公式:


 Y = X + Delta
复制代码


  • X 对应于我们已经编写完毕的基础代码,它不会随需求的变化而变化

  • Delta 对应于额外增加的配置信息或者差异化代码


在这个视角下,所谓的可扩展性方面的研究就等价于 Delta 差量的定义和运算关系方面的研究。


主流的软件开发实践中所使用的扩展机制存在如下问题:


  1. 需要事先预测在哪些地方可能会进行扩展,然后在基础代码中定义好扩展接口和扩展方式

  2. 每一个组件能够提供哪些扩展方式和扩展能力都需要单独去设计,每个组件都不一样

  3. 扩展机制往往会影响性能,扩展点越多,系统性能越差


以 SpringBoot 为例,如果我们在基础代码中创建了两个 bean:beanA 和 beanB,在扩展代码中我们希望删除 beanA,扩展 beanB,则需要修改基础代码,在配置类的工厂方法上增加 SpringBoot 特有的注解。除了直接使用注解之外,SpringBoot 也提供了其他机制可以对 bean 的创建过程进行精细控制,但是这些做法都需要实现 Spring 框架内部的接口,并且了解 IoC 容器的执行细节。如果我们弃用 Spring 容器,换成 Quarkus 等其他 IoC 框架,则针对 Spring 容器所设计的扩展机制就失去了意义。


@Configurationclass MyConfig{    @Bean    @ConditionalOnProperty(name="beanA.enabled",matchIfMissing=true)    public BeanA beanA(){        return new BeanA();    }
@Bean @ConditionalOnMissingBean public BeanB beanB(){ return new BeanB(); }}
复制代码


Nop 平台基于可逆计算原理,建立了一整套系统化的差量分解、合并的机制,可以使用非常统一、通用的方式来实现差量化扩展。特别的,所有的 DSL 模型文件都支持 Delta 定制机制,可以在_delta目录下增加同名的文件来覆盖基础代码中已有的文件。以上面的 Bean 定制逻辑为例,


<!-- /_delta/default/beans/my.beans.xml 将会覆盖 /beans/my.beans.xml文件 --><beans x:extends="super" x:schema="/nop/schema/beans.xdef">
<!-- 删除基础代码中定义的beanA --> <bean id="beanA" x:override="remove" />
<!-- 在已有的beanB的配置基础上,增加fldA的设置 --> <bean id="beanB" > <property name="fldA" value="123" /> </bean></beans>
复制代码


这种 DSL 层面发生的 Delta 合并过程适用于 Nop 平台中的所有底层引擎,例如 ORM 引擎、工作流引擎、规则引擎、页面引擎等都可以使用类似的方式进行扩展定制。运行时引擎完全不需要内置任何关于此类可扩展性的知识。例如,在 NopIoC 引擎中,我们没有像 SpringBoot 那样设计大量的扩展接口,也没有在运行时执行大量的判断逻辑,而是尽量在 DSL 模型的编译阶段以统一的方式来实现动态扩展。


基于 Nop 平台开发的软件产品无需在应用层做出特殊设计即可实现系统级的软件复用,在定制开发的时候可以复用整个基础产品。例如基于 Nop 平台开发的银行核心系统,在不同的银行进行定制化部署实施的时候,不需要修改基础产品的源码,只需要增加 Delta 差量代码即可。通过这种方式,我们可以实现基础产品和定制化版本之间的并行演化


具体技术方案可以参见以下文章:



可逆差量的概念相对比较新颖、抽象,导致一些程序员理解起来存在很多误解,为此我专门写了如下概念辨析的文章:


四. 模板化

目前主流的软件开发实践本质上仍然是手工作业模式,大量的程序逻辑都是通过程序员手工编写完成。如果要实现软件的自动化生产,那么我们必须要使用智能化的代码生成机制来替代手工编写。


Nop 平台基于可逆计算原理将差量化编程和产生式编程(Generative Programming)有机的结合在一起,支持自动生成的代码与手工修正的代码协同工作,通过元编程和代码生成工具渐进式的引入代码生成能力,极大拓宽了产生式编程范式的应用范围。而传统的代码生成方案,一旦自动生成的代码不满足需求需要手工修改,则被修改的代码就脱离了自动化生产流程,被迫成为手工维护的技术资产的一部分,在长期的系统演化过程中,大量自动生成的、结构不直观却必须要手工维护的代码很有可能发展成为技术负债,成为负资产。


代码生成原则上可以使用各种各样的实现方案,只要最终的输出产物满足需求规格要求即可。但是如果希望代码生成的过程尽量维持直观性,而且可以实现代码生成与 DSL 自身的结构表达无缝接驳在一起,我们需要使用模板化的代码生成方案。所谓的模板(Template)就是以目标结构为基础进行模板化加工,在其上增加一些额外的标注,或者将某些部分替换为${xxx}这种形式的动态表达式。


Nop 平台非常强调模板本身与目标结构之间的同态性和同像性(homoiconicity)

同态性

同态性指的是模板自身与输出目标具有相似的结构,最理想的情况下模板本身甚至就是一个合法的目标结构。这方面最直观的例子是 Nop 平台中的 Excel 报表和 Word 报表的设计。它们本质上都是以底层 OfficeXML 为基础,在其上通过注解等内置的扩展机制补充表达式信息,从而将原始的领域对象转换为模板化对象。如果抹去模板对象上的一些扩展属性,我们实际上可以得到一个合法的 Office 文件,并且可以使用 Office 软件来编辑它! 反过来考虑,在实现可视化设计器的时候,如果内置了与模板化相互配合的扩展机制,则我们可以将普通的领域对象设计器增强为模板设计器。


具体方案参见以下文章:



为了保持模板与输出目标之间的结构相似性,在 Nop 平台中我们还系统化的使用了前缀引导语法,在自动化测试的模板匹配语法以及测试数据生成器中都得到了应用。具体参见



当模板自身具有很强的结构化特征时,我们就可以使用 XDSL 通用的 Delta 定制机制来实现对模板本身的定制。

同像性

同像性是源自于 Lisp 函数式语言的一种特性,它指的是模板语言的语法结构和抽象语法树同形,因此可以用模板语言直接生成可执行的模板代码。Nop 平台的做法是采用 XML 语法作为基础结构语法,模板语言采用 XML 格式,它的输出结果也是合法的 XML。例如


<orm>    <c:for var="entity" items="${ormMode.entities}">        <orm-gen:GenEntity entity="${entity}" />    </c:for></orm>
复制代码


保持同像性有如下好处:


  1. 便于引入自定义的程序语法,在使用层面自定义语法和语言内置语法完全等价

  2. 在元编程阶段可以通过统一的 XNode 结构来描述 DSL 语法,对程序结构进行变换和操作普通的 Tree 数据一样简单

  3. 便于跟踪生成代码所对应的模板源码位置。模板语言本身相当于是采用了 AST 语法树形式,它的输出结果不是简单的文本内容,而是新的 AST 节点(XNode)。如果只是文本内容,则没有办法进一步继续进行结构化处理,也没有很简单的方法跟踪生成的代码所对应的模板源码位置。

五. 多阶段分解

Nop 平台大量采用代码生成机制通过自动推理来生产代码,但是如果推理链条比较长,使用一步到位的代码生成方案会导致模型定义过于复杂,而且使得不同抽象层面的信息无序混杂在一起。对于这种情况,Nop 平台提供了一条标准的技术路线:


  1. 借助于嵌入式元编程和代码生成,任意结构 A 和 C 之间都可以建立一条推理管线

  2. 将推理管线分解为多个步骤 : A => B => C

  3. 进一步将推理管线差量化:A => _B => B => _C => C

  4. 每一个环节都允许暂存和透传本步骤不需要使用的扩展信息


例如在 Nop 平台中我们内置了一条从 Excel 数据模型自动生成前后端全套代码的推理 i 管线。



具体来说,从后端到前端的逻辑推理链条可以分解为四个主要模型:


  1. XORM:面向存储层的领域模型

  2. XMeta:面向 GraphQL 接口层的领域模型

  3. XView:在业务层面理解的前端逻辑,采用表单、表格、按钮等少量 UI 元素,与前端框架无关

  4. XPage:具体使用某种前端框架的页面模型


根据 Excel 模型可以自动生成_XORM模型,然后在此基础上我们可以补充差量配置信息形成最终使用的 XORM 模型。下一步,我们再根据 XORM 模型生成_XMeta模型,依此类推。如果写成数学公式,相当于是


   XORM  = CodeGen<Excel> + DeltaORM   XMeta = CodeGen<XORM>  + DeltaMeta   XView = CodeGen<XMeta> + DeltaView   XPage = CodeGen<XView> + DeltaPage
复制代码


整个推理关系的各个步骤都是可选环节:我们可以从任意步骤直接开始,也可以完全舍弃此前步骤所推理得到的所有信息


比如,我们可以手动增加 XView 模型,并不需要它一定具有特定的 XMeta 支持,也可以直接新建 page.yaml 文件,按照 AMIS 组件规范编写 JSON 代码,AMIS 框架的能力完全不会受到推理管线的限制。


基于可逆计算理论设计的低代码平台 NopPlatform 已开源:



发布于: 刚刚阅读数: 5
用户头像

canonical

关注

还未添加个人签名 2022-08-30 加入

还未添加个人简介

评论

发布
暂无评论
Nop平台为什么是一个独一无二的开源软件开发平台_开源_canonical_InfoQ写作社区