认知程序设计-【复杂度治理】破解通用业务域声明式

认知程序设计-导读

概述

认知设计是一个软件设计新思路,设计目的是降解应用系统的复杂度。实现结果超越了设计预期:通用业务域竟也能和声明式一样:只需描述结果,实现过程完全交给框架。下面列出了框架实例的重要指标表现,详情可见《五 表现特性》。

复杂度表现

代码可读性

性能表现

节点导读

一 设计起点

新思路设计的起点是语言在认知中的作用,以及哲学中对世界认知的观点:"表象由本原构成"。据此引出认知设计的核心要点:表象由本原构成的三个逐级明确的关系。后续的框架设计完全由这三个关系推导确定。

二 设计详解

<设计起点>其实已经完成了所有的设计确定性。然而它过于简单粗暴,显得有些空洞。《设计详解》节点描述了与现有设计最核心的差异点:没有过程的概念。

实际上现有设计一直都在努力的把过程剔除出去,比如函数式编程、DDD设计,但均未彻底(未能像声明式编程那样只需描述结果,而把过程实现完全交给框架)。现有设计其实无法破局:编排的对象是功能,而功能自身就蕴含着过程。破局需要一个新概念:此概念既能指引编排,又不蕴含过程。而认知设计恰好提供了这一概念。

程序设计一直努力驱逐的过程顺序,它从一开始就没在认知设计的世界里

三 框架实例

包模块依赖图、核心类定义、以及功能实现流程图。

四 代码组织

当前的代码组织以功能为中心,直观表现为功能流程图。认知设计以构成关系为中心,直观表现为表象构成图。实质上是把流程图分离为标准流程图和表象构成图。构成图是比流程图更纯粹的业务逻辑描述。

五 表现特性

复杂度,代码可读性,性能。

一 设计起点


1.1 用语言直接描述对世界的认知

认知设计想要解决的问题是应用系统的复杂度。

认知设计的观点:语言直接描述对复杂业务的认知,而把功能的实现交给系统框架。


1.2 对世界的认知

  1. "世界是我的表象" - 叔本华的观点。
  2. "世界的本原是水、气、原子等等" - 早期自然哲学流派的观点。

这里的观点是:世界皆为表象,表象由本原构成


1.3 认知设计的核心要点

1.3.1. 概念分为表象和本原

  • 表象 :直观感受到的现象,这些现象可能带有比较凌乱的属性。
    示例:载货车辆、转账现象;前后台交互的内容都可视为表象。
  • 本原 :对表象进行理性分析后,所理解到的表象所构成的基础单元。
    示例:转账现象的本质是两个账户的值发生了改变,转账现象的本原概念就是账户。

表象与本原参考了聚合根与实体,但表象与本原的关系更着重于对现象的更深层的思考和挖掘到的本质:用更少的本质规律决议更宽广的现象空间。

1.3.2. 表象由本原构成

构成关系分为三个逐级明确的关系:

(1)表象所关联的本原集

这第一层仅描述了关联的类型范围和数据范围。

示例:载货车辆(表象)所关联的本原类是车辆和货物;某一载货车辆,关联的车辆车号xxx、货物编号xxx。

(2)表象由本原集构造而成,本原集由表象拆解而来

这第二层关系描述了表象各属性与本原集各属性间的函数关系。

这些函数关系包括未能自明的自然规律、常识约定、业务约定等。

示例:载货车辆的质量 = 车辆质量 + 货物质量。

(3)表象变化的实质是本原集的属性变化

这第三层关系描述的是现象属性变换与本原属性变换之间的函数关系。

示例:发霉现象的本质是其本原(微生物组分)生长繁殖。

二 设计详解


2.1 没有过程的概念

认知设计的核心要点中只提到了表象与本原的关系,但未提到表象与本原间转换的过程。这与代数方程组很像:只是列出了方程组,未执行解方程。

在具体的实例中表现为:业务应用里只有关系描述的 morphism(domain进阶) 包,没有过程执行的 service 包,service 只存在于底层框架。

直白概括来说,就是执行过程不再属于业务逻辑。这一点是认知设计与现有设计最大最核心的差异。

在后面的实例中将会看到:业务逻辑在剔除过程之后,应用性能直接起飞:从默认的串行变成了高度并行,响应时长降到原来的 1/2、1/3...1/6。大事务、依赖透明度等问题都原地消失。

但是在那之前,这里还是需要深入分析藏匿在逻辑中的过程,到底是怎么一回事。

举个载货车辆的例子。载货车辆的质量 = 车辆质量 + 货物质量,在现有设计中,载货车辆的质量是通过"车辆质量+货物质量"赋值得到的。实际上,这个赋值包含两层含义:

  1. 赋值时所依赖的函数关系:质量函数关系;
  2. 赋值执行过程。

但是在真实的物理世界中,只有第一层含义真实存在。"载货车辆的质量=车辆质量+货物质量"是固化在事物内部的固有关系属性。而"载货车辆的质量"并不需要任何人执行赋值,也根本没有任何赋值过程,它自身就已经有值。简单而言:真实世界只有函数关系,不存在赋值过程。

上述观点可追溯到哲学史上"用函数关系代替因果关系(因果过程)"的演进:

  1. 1710年,贝克莱把客观因果拆解为"观念间的恒常联系";
  2. 1748年,休谟论述因果关系并不真实存在,它只是经验到许多次A到B的相继过程后的习惯性期待;
  3. 1798+,孔德提出科学只描述现象规律,不追问无法实证的因果关系;
  4. 1886年,马赫提议用函数关系代替因果关系,因为自然界里没有原因和结果。函数不解释因果,只描述现象。

马赫认为因果是原始、粗糙的思维工具,而函数关系才是科学、精确、无玄学的描述。回到软件设计,函数关系也才是业务逻辑科学、精确、纯粹的描述。


2.2 函数关系是未来框架形态必经之路

在降解复杂度的框架演进上,三级分层成熟于2005年,至今已21年。DDD于2016年重新燃起,但90%的DDD项目都是"三层结构+一堆DDD名词"的伪DDD,并不能算成功。至今也已10年。

框架演进的下一个形态是什么样,这是个艰难的问题。但有一点可以预见:必然要经历一个把过程从业务逻辑中剔除,只留下纯粹函数描述业务的纯关系形态。 下面将从数学史、编程思想史、软件实现演进来进行分析预判。

数学史上解决复杂问题的方法与软件设计不谋而合:抽象与封装。

  • 15世纪以前,全人类都在用算术步骤解题:已知数 → 一步步算术 → 中间量 → 最终答案;
  • 1591年前后,韦达成功把"一步步算术"中隐含的未知量关系抽离成方程,并提供标准的步骤解法;
  • 当下的软件功能实现与数学史算术阶段非常相似:已知入参 → 一步步赋值 → 中间量 → 最终数据集;
  • 未来框架的必经形态:从"一步步赋值"中抽离出纯粹描述关系的方程,由框架实施标准解过程。

编程思想史上对降解复杂度有特别贡献的面向对象思想,渊源于公元前350年亚里士多德,《范畴篇》给出了概念的定义形式:种+属性差。哲学史上的每一个观点突破,都是对世界更深层次的理解突破。这些理解突破若能与编程实践结合,必然能降解复杂度,成为框架演进的必经形态。哲学史上的"用函数关系代替因果关系"就是这样一个若隐若现的突破点。

软件实现演进上,声明式编程(各种领域DSL,如sql,html)正在向各个领域渗透。 它只描述结果,过程交给系统实施。现在的软件实现,正逐步的把过程实现抽离成结果描述。在通用业务领域,能够对结果进行描述的,也即结果必须符合的函数关系,是框架演进的必经形态。。


2.3 标准步骤解法破局:编排对象视为函数关系

编程思想史上一直都在努力的把过程剔除(去除goto等),实际上也已成功出抽离出不包含过程的函数关系(函数式编程)。而在寻找标准解步骤的道路上也做了很多的探索和尝试,比如DDD所抽离出来的Application Service应用编排层,低代码的流程编排等。

上述的编排,都是把编排对象视为功能来进行编排。而功能这个概念本身就蕴含过程和实现。也既未能把过程剔除干净,故而不存在标准解步骤。

可以破局的一个点是:不把编排对象视为功能,而仅仅视为函数关系。但若不视为功能,又是依据什么来进行编排呢? 这是个艰难的问题。

世界皆为表象,表象由本原构成。 这是对"万物皆对象"的补充描述。同时也给函数关系赋予了另一含义:函数是表象与本原之间的函数关系。 而依据这一层含义,可以对函数关系进行编排,真正的达成标准解步骤。

依构成关系来编排函数 vs 依功能组成来编排功能: 功能是原始的、粗糙的思维工具,而构成关系才是纯粹的、更高抽象级别的思维工具。(功能概念蕴含过程,而构成关系不蕴含过程)。

三 框架实例


3.1 包结构依赖图

框架:

  • morphism.jar: 态射包。 domain的进阶版。表象与本原定义,表象与本原间三个核心关系定义。是整个认知设计最核心的定义。(其他包都可以自行实现。)
  • service.jar: 服务包。只存在于框架层。里面是标准解步骤。
  • dao.jar: 存取适配包。只存在于框架层。是service调用各具体dao.impl的适配桥梁。
  • dao.impl.xxx.jar 存取实现包。框架提供的数据存取基本实现。(可选)

实际项目(典型):

  • morphism.jar: 态射包。表象与本原间的函数关系集。(态射:保持某种结构不变的映射)
  • api.jar: web接口之外的开放接口包,供别的项目的rpc调用。
  • web.jar: web接口服务包,执行标准流程以及与morphism间的适配转换。
  • dao.impl.xxx.jar 数据存取实现包。

3.2 核心定义

morphism.jar:

  • 本原定义: Principle.java 里面只有属性id和name, 非常简单。

  • 表象定义: Appearance.java 这是一个接口。定义了表象由本原构成的三个逐级明确的关系:

    1. qualifiersLanes方法:表象所关联的本原集
    2. construct方法+deconstruct方法:表象由本原集构造而成,本原集由表象拆解而来
    3. transforms方法:表象变化的实质是本原集的属性变化

这里的定义完完全全遵照《认知设计的核心要点》,只字不差。


3.3 功能实现流程

核心流程:

  • Service.java 服务流程中只执行标准的解步骤,一共6个标准步骤。已在上图中用①-⑥标出。

实现细节补充:

  1. 关系1所返回的本原集定义,并非是查询条件,而是能返回查询条件的函数序列。这些函数序列能解决查询依赖问题(后一次查询条件依赖于上一次查询结果)。
  2. 关系2之二返回的本原集,也只是函数序列(后一次要写入的数据依赖于前一次写入的结果)。
  3. 没有依赖关系的数据查询,底层默认执行并发查询; 没有写入依赖关系的结果数据,底层默认执行并发写入。(线程池支持本原(表)粒度的读写配置)
  4. 写入数据支持事务组自由编排。

附录

四 代码组织


当前的软件实现,以功能为中心进行代码组织,其直观表现为功能流程图。认知设计没有过程的概念,也几乎没有功能的概念, 以构成关系为中心进行代码组织,其直观表现为表象构成图。


4.1 表象构成图

1) 三大泳道:
  1. 原象集: 构成关系所依赖到的本原类型
  2. 表象(辅助变量):一个表象可能会很复杂,需要经过若干次模型变换才可达成。
  3. 映象集: 构成关系中所拆解到的本原类型。
2) 中间模型行:
  1. 中间模型定义。就是普通的模型类,位于行中间。
  2. 此模型依赖的本原取值列表, 位于左侧。(也可能有依赖于上一行的模型)。
  3. 此模型拆解的本原存储列表,位于右侧。
3) 中间模型:
  1. 中间模型就是代数方程法中的中间未知量(向量)。
  2. 模型包含构造方法:以左侧本原集以及依赖的上方中间模型为入参,构造出中间模型。
  3. 模型包含get方法: 以自身所包含的信息为主,拆解成所需更变保存的本原集。
附 构成图实例:
  1. 文章查看表象 (简)
  2. 文章版本提交 (易)
  3. 版本审核通过 (难)

4.2 流程图 vs 构成图

构成图的左侧,明确的列出此业务逻辑所依赖的全部因素。构成图的右侧,明确列出此业务逻辑所影响到的全部范围。而流程图中对因素和结果的描述,依赖于交互箭头上的文字描述,无法保证其规范性。

认知设计实际上是把流程图分离成了标准流程图和表象构成图。把业务开发从见首不见尾的流程淤泥中解放出来,真正的专注于业务规则的描述与开发。

五 表现特性


5.1 复杂度降解

复杂度降解的历程,是不断的把过程剔除掉的历程。过程本身就是软件复杂度最大的来源。过程性含量是复杂度最重要的指标。过程性含量虽然未能量化,但可以定性对比。

  • 结构化、模块化把goto剔除。
  • 面向对象用对象替代过程中心;函数式剔除赋值过程。
  • DDD和低代码都把流程抽离到编排。
  • 声明式中只有对结果的描述,基本没有过程。
  • 认知式把编排对象从功能净化为函数关系。

标准步骤解是对过程性含量的终极校验: 如果一个系统仍包含无法确定的过程,亦即无法归化为标准步骤,就不存在标准解。 例如DDD无法把流程编排归化为标准编排。

认知式第一次把流程编排归化为标准解步骤,达成与声明式一样效果:只需描述结果,实现过程完全交给系统框架。而它们的区别是,声明式是特定领域,而认知式是通用业务域。


5.2 代码可读性

5.2.1 依赖透明度

依赖透明度的一个重要含义是指函数返回值所依赖的因素是否都明确的列在入参里。是代码可读性的重要指标。

过程式service类中的方法,可以在方法执行的过程中随意读写数据源,故而影响返回值的因素并不全在入参里,属于不透明依赖。认知式以显示声明定义需要读取的数据,以及需要写入的数据。根本无法直接操作数据。函数返回值不受外界数据影响,依赖因素全在入参里,是透明依赖。

5.2.2 影响因素可见性

依赖透明度是对单个函数而言的。在更大的视野里,比如一项操作,或者一个api接口的粒度上,可以引入因素可见性的概念:一个业务单元执行结果所依赖的全部因素,能够以某种方式完整的列出来,那么可以认为这个执行单元的影响因素是可见的。影响因素可见性是依赖透明度在更大尺度上的延伸指标,在代码可读性上同样具有重要意义。

函数式编程虽然每个方法都是依赖透明,但在若干方法组成的执行单元中,并没有特别的机制把这些依赖都列在一个地方。认知式的观点是,表象由本原集构成,也就是可以把构成因素明确的列在表象属性里,进而达成影响因素可见性。虽然框架层面并未强制约束,但确是随手即得的最佳实践。

5.2.3 影响结果可见性

类似的可以定义影响结果可见性:一个业务执行单元所有要写入(包括更新删除)的数据,能以某种方式完整的列出来,那么可以认为它具有影响结果可见性。


5.3 性能表现

  • 过程式的耗时:每次数据交互时长的累加。
  • 认知式的耗时:接近并行度极限,值为最长的一组数据交互时长。
  • 函数式的耗时:介于两者之间。

过程式的业务逻辑与数据操作是串行的。认知设计里没有过程的概念,天然支持并行。但它无法逾越数据依赖(第二次查询的条件依赖于第一次查询的结果)。

函数式使用并发比 过程式容易得多。尤其在多 dao 并行这类场景中,比如函数式ZIO框架 通过原生内置的并发算子(如zipPar)实现零配置并发,而 Spring 需要额外的线程池配置、注解和异步结果编排。

相关推荐
程序员寒山13 分钟前
小龙虾养成日记:“虾”路相逢之Openclaw九大安全防护策略
程序员
嵌入式小企鹅4 小时前
算力价值重估、AI编程模型齐开源、RISC-V融资15亿
人工智能·学习·ai·程序员·risc-v·前沿科技·太空算力
小兵张健13 小时前
一场大概率没拿到 offer 的面试,让我更坚定去做喜欢的事
人工智能·面试·程序员
阿虎儿21 小时前
56条软件工程开发定律
程序员
程序员鱼皮1 天前
Git WorkTree 是什么?凭什么能让 AI 编程效率翻倍?
git·ai·程序员·编程·ai编程
czkm1 天前
AI有情绪吗?从AI夸我是写作领域大神说起
人工智能·程序员·ai编程
SimonKing1 天前
AI编程工具装了一大堆,Skills 管理乱成粥?这个开源神器一招搞定!
java·后端·程序员
小兵张健2 天前
AI 带来的机遇,可能真的大于风险
程序员·openai·ai编程
WebInfra2 天前
Rsbuild 2.0 发布:即将支持 TanStack Start
前端·javascript·程序员