Skip to content

Commit

Permalink
增加文档
Browse files Browse the repository at this point in the history
  • Loading branch information
entropy-cloud committed Jul 22, 2023
1 parent 0d6f787 commit f293456
Showing 1 changed file with 166 additions and 5 deletions.
171 changes: 166 additions & 5 deletions docs/theory/pros-and-cons-of-framework.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 如何评价一种框架技术(比如ORM框架)的好坏
# 如何评价一种框架技术的好坏

一个很有趣的问题是,一个新的框架技术出现的时候,我们如何评价它的好坏?NopORM引擎今年开源以来,也收到了一些反馈,不过大部分人应该没有看懂NopORM引擎的理论部分,所以普遍的疑惑是NopORM相对于其他ORM引擎到底有什么具体的优势? 在本文中我想谈一谈评价一个框架可以有哪些不以人的经验、喜好、熟悉程度为转移的客观标准。

Expand Down Expand Up @@ -108,7 +108,7 @@ Hibernate出道即巅峰,它确立了所谓ORM框架这个品类的基本形

6. 自动利用外键关系推导出多表关联条件,比如where a.b.c = 3会自动利用a和b的关联条件以及b和c的关联条件自动生成a、b、c这三个表的关联条件。如果前台已经实现了针对单表的查询显示,则将字段名修改为a.b.c就自动可以实现多表联合查询。

NopOrm框架引入了更多的自动推理
NopORM框架引入了更多的自动推理

1. 自动将纵表转换为虚拟的横表:扩展字段可以保存在(entityName,entityId,fieldName,fieldValue)这样的纵表中,但是程序中使用时与数据库原生字段相同,同时可以通过SQL语法实现对扩展字段的查询和排序。

Expand All @@ -128,28 +128,189 @@ NopOrm框架引入了更多的自动推理:

这里的自动转换指的是信息的核心内容并没有发生变化,只是从一种表示形式转换为另一种表示形式。自动转换本质上是信息自动推导的一个特例,但是因为它的设计非常通用,所以值得单独强调一下。

最典型的例子是Web框架中常用的JSON转换:Java对象和JSON文本之间的双向转换。早期的Web框架缺乏规范化的复杂参数编码方案,很多情况下我们需要手工编程解析前台发送的请求参数。而现在JSON序列化已经发展成为一个脱离Web环境的通用结构转换方案。

因为自动的双向转换意味着信息总量保持不变,所以一般情况下它都是与具体业务无关的一种通用机制。从数学上说,这意味着对于结构空间A中的**每一个结构**,我们都可以在结构空间B中找到它的一个对应结构(或者一组等价的对应结构),反之对于结构B中的每一个结构,我们也能够在结构空间A中找到它的对应。

1. Nop平台可以实现XML和JSON之间的自动转换。在Nop平台的前台我们引入了百度AMIS框架,并可以使用XML、JSON、YAML等多种文件格式编写AMIS页面代码
自动转换可以很自然的连接在一起形成更为复杂的复合转换

2.
```
A <=> B <=> C
```

需要强调的是,这种转换是通用的,也就是说**不存在特例,每一个可能会遇到的结构都能够被转换**。而我们手工针对业务场景进行编程时使用的转换方式一般都是AdHoc的特殊实现,无法自动处理其他业务场景下的情况。

基于可逆计算原理,Nop平台将自动转换的设计思想贯彻到了平台的方方面面:

1. 实现XML和JSON之间的自动转换。因此我们可以使用XML、JSON、YAML等多种文件格式编写AMIS页面代码(AMIS框架本身只支持JSON格式)。

2. 实现XML和领域对象之间的双向转换。因为我们只要定义XDef元模型就可以自动实现领域模型解析、验证、断点调试等功能。

3. 实现Excel和领域对象之间的双向转换。因此我们可以使用Excel格式的模型文件来设计平台中的所有领域模型,而无需特殊编写模型解析代码。比如,我们可以根据XML格式的ORM模型定义自动得到Excel格式的数据模型定义,或者反之。

4. 实现可视化编辑模型和领域对象之间的双向转换。只要通过简单描述即可自动实现领域对象的可视化编辑。比如,工作流的设计器和ORM设计器都可以根据领域模型的定义自动推导得到。

```
YAML <=> JSON <=> XML <=> DomainObject <=> Excel <=> VisualModel
```

      反观MyBatis、Hibernate、Spring等框架,它们对于模型文件的解析都是手工实现的,IDE插件和可视化编辑器等都需要单独去编写维护。

还有一些双向转换并不是完全的信息等价,我们可以在转换过程中补充一些额外的信息。比如前台提交的请求数据可以被自动转换为数据库实体对象,从而实现主子表数据的保存和修改,但是这个转换过程需要进行数据有效性检查和权限检查,执行类型转换、格式转换等操作。另一个方向,取出数据库实体之后我们可以将它自动转换为JSON数据返回给前台,这个过程同样需要进行权限检查和格式转换。NopGraphQL引擎通过引入Meta对象来补全信息差,从而将复杂业务对象的增删改查操作自动化。

```
JSON + Meta => Entity
Entity + Meta => JSON
```

### 四. 在框架之外如何使用相关信息?

传统上的框架设计只关注自身的功能特性,对于框架在外部信息网络中的位置和交互价值往往并不关心。但随着软件领域智能化程度的提高,我们希望推动信息在各个层面、各个组件之间无阻碍的自由流动,此时必须要考虑框架和外部信息网络如何相互作用的问题。

### 独立存在的模型信息

有一定复杂度的框架一定会建立自己的领域模型,并在内部大量使用可配置的模型信息,比如Hibernate内部使用的EntityModel,Spring内部使用的BeanDefinition模型等,但是在很多框架中,模型信息都只具有内部表现形态,与框架的运行时紧密纠缠在一起,外部系统无法通过很简单的方式来复用这些模型信息。

Hibernate在6.0之后逐步开始废弃hbm配置文件,仅保留注解作为实体模型定义方式,这在某种程度上是一种倒退。

1. 它使得Hibernate所定义的实体模型被封闭在Hibernate框架之内,如果不使用Hibernate内部的实现我们很难获取到对应的模型信息,而通过Hibernate内部函数获取到的模型对象上也有可能不是纯粹的描述性信息,而是混杂着与Hibernate的运行时实现相关的其他信息。

2. 如果我们选择直接通过反射去解析注解信息,同样会依赖于Java语言内置的机制,同时我们需要进行过滤筛选,屏蔽Java对象上与实体模型不相关的信息,比如需要忽略transient字段,忽略不相关的其他注解等。也就是说我们需要重新去发现**模型信息**,而无法直接获取到一个解析好并通过验证的模型对象

与Hibernate的做法相反,Nop平台强调模型信息的独立性,**模型信息以XML文件为载体**。这样其他的语言或者框架可以在完全不依赖Nop平台的情况下自由的利用这些模型信息。比如代码生成器可以直接读取XML模型文件,生成前后端代码,生成Word或者Excel文档等。

> 模型对象序列化为模型文件,而模型文件必然具有某种约定的Schema结构,它可以被看作是一种特定的语法定义,即所谓的领域特定语言(DSL)。
### 独立诊断和调试

另外一种常见的情况是框架内部存在一个非常复杂的模型构造过程,比如SpringBoot框架内部会执行复杂的条件判断,最终起作用的具体是哪些Bean的定义并没有一个直观的展现,这导致出现问题的时候难以进行诊断。这很自然的引出一个问题:如果完全不了解框架的执行细节,**从纯粹的外部视角去观察系统**,我们可以得到哪些信息?

Nop平台基于可逆计算原理,广泛使用DSL来定义和描述系统功能,它明确区分了编译期和运行期,将尽可能多的与运行时无关的计算下放到编译期执行,通过所谓的元编程(Meta Programming)来完成模型信息的动态发现和组装。

例如,NopIoC框架在启动时会执行类似SpringBoot的动态条件判断,得到的结果是一个统一的BeansModel。在调试模式下,框架会自动输出一个合并后的模型文件到`_dump`目录下,同时我们也可以通过`/p/DevDoc__beans`这种的REST服务来获取到对应的模型定义。返回的模型定义采用Spring1.0语法格式,相当于是NopIoC通过元编程将具有动态条件的模型语法化归为简单的Spring1.0语法。通过合并后的beans.xml文件,我们就可以直观的理解当前系统中实际被启用的bean的定义情况。

Nop平台中所有的模型对象都可以自动的转换为对应的DSL模型文件。这些模型文件完全没有运行时的状态约束,可以被直观的理解。系统执行异常时,Nop平台也会尽量将异常对应于DSL所在的源码位置。

### 信息管道

如果我们跳出框架自身,从更广阔的信息传递网络角度去观察,我们会发现框架不仅仅是信息的源(Source)和汇(Sink),很多时候它也需要作为信息的通道,也就是说某些信息并不由框架产生,也不由它消费,但是可能需要经过它传递给其他的结构(特别是在分层架构中框架处于某一抽象层面时,原则上需要避免跨层面直接交互)。

在Nop平台的设计中,所有的DSL和所有的实体对象都自动支持扩展属性,可以存放当前框架不会直接用到的扩展信息。从而在整个框架中始终维持着一条扩展信息通路。这是一种全局性的设计,它意味着在系统各处都需要有额外的信息存放空间,比如经过消息队列发送信息时,我们要求消息必须支持header集合,可以通过header存放额外的信息。

借助于扩展属性,我们在构造如下的软件生产管线就可以在上游指定部分在下游使用的信息:

![](https://gitee.com/canonical-entropy/nop-entropy/raw/master/docs/tutorial/delta-pipeline.png)

在Excel的数据模型中我们可以指定部分显示相关的属性,它经由ORM层的扩展属性传递给Meta,然后再传递到前端页面模型中。



### 五. 框架的设计完整性如何?

从科学的角度上说,一个科学的解决方案绝不会是一个孤立的设计,而必然是包含从简单到复杂的一组可以渐进演化的策略集合,针对不同的复杂性我们都需要拟定对应的解决策略。因此,一个立足于适应各种使用场景的底层框架一定要在某种程度上保证自己的设计完整性,而且这种完整性往往无法用穷举法来实现。



### 分层设计

举一个简单的例子。假设我们现在要编写一个流程设计器,流程节点需要显示图标和文字,最简单的设计如下:

```javascript
type Node{
icon: string;
label: string;
...
}
```

如果我们需要控制文字的显示位置,则还需要加入labelPosition这样的描述字段。如果要求根据流程状态的不同改变背景颜色或者显示额外的一个状态标识,则我们需要继续为节点增加statusIconMapping等属性。显然我们是无法通过穷举法来响应所有需求的。一个合适的解决方案是在节点级别提供一个渲染函数抽象,它负责实现节点的自定义渲染。

```
render(data){
...
}
```



### 异步处理

同步处理和异步处理看似只是技术层面的一种选择,但本质上它们对应着不同的世界观。一个没有考虑异步处理的框架设计是不完整的。

在目前的程序实践中,如果事前没有考虑支持异步处理,则往往后期很难将整体框架转换为支持异步处理。为了系统化的支持异步处理,我们需要考虑如何将一个上下文对象在各个线程间进行传递的问题,同时也需要考虑各种并发场景下如何避免锁冲突等复杂的技术问题。



## 六. 框架提供了哪些差量化机制?

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

```
Y = X + Delta
```

* X对应于我们已经编写完毕的基础代码,它不会随需求的变化而变化
* Delta对应于额外增加的配置信息或者差异化代码

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

现有的框架技术所使用的扩展机制存在如下问题:

1. **需要事先预测在哪些地方可能会进行扩展,然后在基础代码中定义好扩展接口和扩展方式**
2. **每一个组件能够提供哪些扩展方式和扩展能力都需要单独去设计,每个组件都不一样**
3. **扩展机制往往会影响性能,扩展点越多,系统性能越差**

以Hibernate中增加Gis扩展为例,需要实现ContribuerImplementor接口,实现contributionFunctions等函数,在其中注册GIS相关函数。

```
@Override
public void contributeFunctions(FunctionContributions functionContributions) {
HSMessageLogger.SPATIAL_MSG_LOGGER.functionContributions( this.getClass().getCanonicalName() );
KeyedSqmFunctionDescriptors functionDescriptors;
if ( useSTGeometry ) {
functionDescriptors = new OracleSQLMMFunctionDescriptors( functionContributions );
}
else {
functionDescriptors = new OracleSDOFunctionDescriptors( functionContributions );
}
SqmFunctionRegistry functionRegistry = functionContributions.getFunctionRegistry();
functionDescriptors.asMap().forEach( (key, funcDescr) -> {
functionRegistry.register( key.getName(), funcDescr );
key.getAltName().ifPresent( altName -> functionRegistry.register( altName, funcDescr ) );
} );
} sdoGeometryType );
}
```

**在Nop平台中我们可以通过统一的Delta定制来实现扩展**。具体介绍参见[如何在不修改基础产品源码的情况下实现定制化开发](https://zhuanlan.zhihu.com/p/628770810)

### 七. 整体代码量是否更小?

# 总结
一个可以进行客观度量的标准是:使用框架之后,代码量是否发生了下降?代码量可以看作是对系统复杂性的一种度量(所谓的描述复杂性)。一个有价值的框架它应该可以降低系统的复杂度,从而使得代码量出现明显的下降。

这里容易出现误导的是,我们必须考虑模型和应用两者的代码量之和:

```
整体复杂度 = 模型复杂度 + 应用复杂度
```

很容易想见,我们可以实现一个非常复杂的模型,把所有常见的业务需求都做成开关(穷举法),这样只要输入少量应用描述即可得到实现应用系统。另一个极端是,我们可以实现一个功能非常贫瘠的模型,然后完全依靠在应用层编码来实现业务需求。显然,我们需要实现这两者之间的平衡,让模型的复杂度与具体业务应用的复杂度相匹配,这样才可以保证模型可以在尽可能多的业务场景中发生作用(提升模型的泛化性能)。

# 总结

* 一个框架相当于是建立一个有独立存在意义的技术空间,它所提供的各种能力相当于是这个空间中定义的运算规律(数学定理)。

* 类似于数学定理的推导,在越少的假设上进行的推导(摆脱对具体业务上下文的依赖)可以应用到越广泛的场景中。

* 自动推导的结果可以像数学定理那样复合起来,得到新的结果。

* 可逆计算理论提供了对设计完整性的一种新的评估视角。

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

- gitee: [canonical-entropy/nop-entropy](https://gitee.com/canonical-entropy/nop-entropy)
- github: [entropy-cloud/nop-entropy](https://github.com/entropy-cloud/nop-entropy)
- 开发示例:[docs/tutorial/tutorial.md](https://gitee.com/canonical-entropy/nop-entropy/blob/master/docs/tutorial/tutorial.md)
- [可逆计算原理和Nop平台介绍及答疑_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1u84y1w7kX/)

0 comments on commit f293456

Please sign in to comment.