程序员活跃的是语言中枢 —— 抽象与结构关系

再次声明,从这里开始,已经不会主要讨论开发技术了。还是那句话,技术提供可能性,现在只讨论战略和思想

抽象

抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程

我们常见的,最能体现抽象思维的例子,就是语言

我们说出’牛‘的时候,会有一头真的牛从天而降么?不会!

公孙龙说出:”白马非马,可乎?“的时候,不管是’白马‘和’马‘都不会从天而降!

因为这些概念只存在于我们的脑中,我们的大脑不会存储一匹马,一头牛的完整信息,只会存储他们的特征和行为

这不是面向对象(虽然常被作为面向对象的例子),这比面向对象更加本质,这是”抽象形式系统

我们通过语言交流,通过UML等工具,进行建模的过程,本质上,是吧现实世界中的事务,转化成思维世界(抽象)中的事务,然后我们通过语言和相关工具,形成广泛的合作

而程序员的工作,就是将抽象的概念,重新降维成现实中硬件的电流,是的,这就是翻译的工作!

既然是翻译,技术实现只是我们工作的很小的一部分,比如你是个英汉传译,汉语水平要求,并没有英语水平要求高

同理,程序员业务建模,算法/数据结构水平的要求,也远比具体技术平台能力的要求高,具体技术平台只是细枝末节(这个看法也是微服务的直接起源)

是的,那些琳琅满目的 if else,for range, async await 甚至 ch -> , ch <- 都不是程序员的本职工作,分析业务才是

因此,那些广泛存在于抽象形式系统中的概念,才是我们最应该研究的东西,比如:

抽象思维阶段: 归纳->演绎->推演

  • 归纳即为形式系统创建过程,class,monad function 都是如此

  • 演绎,则为形式系统创建关联关系的过程,设计模式,类型等皆是如此,甚至以后提到的事件风暴,测试驱动,也是这个过程

  • 推演,即在此系统基础上,在没有现实提供依据的情况下,给出预测,这部分就是开发的高阶运用的,比如背包问题,贝叶斯预测,人工智能等算法问题

其中,开发平台基础设施,开发范式决定了归纳过程

是的,形式系统目前主要有两种,图灵式面向对象,lamda演算函数式,前者是观念的抽象,后者是变化的抽象,设计模式中将其称为:构建型模型

而推演是极致抽象思维,数学和算法称霸的领域,不在目前讨论范围内

演绎,我们先弄清楚,归来出来的概念间,会有什么样的关系:

依赖,关联,聚合,组合

依赖关系:引用调用关系,表现为函数形参,import等

image.png

关联关系:又称强依赖,表现为成员变量,map等

单向关联
image.png

双向关联
image.png

自身关联
image.png

多维关联
image.png

关联和依赖的区别在于,是否可以脱离,比如 import 了,我可以不用,函数形参,我也可以不用,但是如果是关联,箭头所指方无法脱离另一方单独存在

一个很好的例子,你只是送了一个女孩子礼物(依赖),她不一定会和你成为男女朋友(关联)

聚合关系:集合实体关系,也称强关联关系,即几个关联实体间形成一对多不平等关系,比如数组和数组元素,对象和成员变量

image.png

组合关系:整体部分关系,也称强聚合关系,要求整体负责部分的创建和销毁,整体能够完全控制部分(公开接口),比如成员对象,monad 等

image.png

当然,还有泛化,即强组合,实体默认带有祖先状态+行为,实体可以访问祖先(protect 接口),但是,这个层次的关系完全没有必要,是一种思维执念,抽象层次的隐藏实现是不好的方案,组合优于继承

在组合中,控制方向是从整体到部分,即成员在初始化时,无法访问初始化它的实例(子组件无法访问父组件数据,调用父组件行为),这个时候,如果提供一个整体实例的引用给子实例,相当于一个依赖箭头被指向了子实例,那么,子实例也能够调用父实例的行为和状态,这种做法,叫做控制反转

// 函数 props 控制反转

const [a,setA] = useState()
// jsx 实际上是函数,这里将 setA 的引用传递给了 Child
// 虽然付父组件控制 Child 的生命周期,也就是说,父组件和 Child 是组合关系
// 但是因为 props 的控制反转,Child 能够间接控制父组件的逻辑
return <Child setA = {setA}/>
复制代码

注意,控制反转本质,实际上是在 子实例 中书附加 父实例逻辑,需要谨慎处理本地和外部逻辑关系,这部分的方案叫做”领域事件“,之后会讲道

这些关系上的逻辑,和开发的具体技术毫无关系,任何平台任何技术,甚至产品设计,都必须遵循这个标准

从上面的分析,我们可以发现,关联和引用太过微观,应用开发基本上的结构,就是在搭配使用 聚合 和 组合

怎么个搭配法呢?

聚合在外,组合在内!

为何如此?因为聚合是松散耦合,便于进行模块划分和模块间通讯,组合是强耦合,便于实现具体功能点

这边是高内低偶的做法

很多人习惯不了 useContext 的用法,导致践行 DDD 出了很多问题,有更大的思维负担的原因,就是因为没有区分聚合和组合,这一点在面向对象范式中并不会出现(因为组合天然是 class)

比如,以下做法,是组合关系(控制初始化和销毁,即没有整体,没有部分)

// 组合 hooks
function useOverall(){
   const partial = usePartial()
   const partial2 = usePartial2()
   const partial3 = usePartial3()
} 
// 组合 component
function Overall(){
  return <>
     {/* 这种控制反转,叫组合控制反转 */}
     <Partial onChange={xxx}/>
     <Partial/>
     <Partial/>
  </>
}
复制代码

是的,组合关系我们不用 context,因为 context 和 useContext 是松散耦合,子实例(以后叫子域)的初始化并不受父域的控制,话句话说, 我们说 context 是控制反转,特指 聚合控制反转 ,我们说 props,组合 hooks 参数,高阶组件 是控制反转,特指组合控制反转

// 聚合 hooks,松散耦合,子域的初始化并不受其控制
const token = createContext()
function useAggregation(){
   // ...
}
复制代码

因为聚合有低耦特性,因此,我们在讨论业务的时候,优先采用聚合建模,即默认所有宏观关联关系,都是聚合关联关系

聚合之间,可以按照功能,领域,特性,形成一个领域模块,所有领域的根,被称为聚合根

为什么会有领域模块的概念?毕竟所谓 功能,领域,特性 等描述,太过于笼统,有什么更清晰的标准么?

统一语言

没错,就是统一语言,这个就是领域模块划分标准

我们日常交流的过程中,都默认带有一个上下文,不同上下文中的同一词汇,拥有不同的含义

这是我们意识不到的常识,即每种抽象概念,都只有它相应的适用范围,每个语言的词汇,都有近义词,专业名词等一系列的泛化变化

比如用户下单的”订单“,和仓储物流的”订单“,完全就是两个概念!

意识不到这是两个概念?那就只能等到实现的时候,才发现他们的不同咯?

这种语言依赖的上下文,被称作模块的限界上下文,它是模块划分的唯一标准

这部分先不展开,因为他是面向业务开发结构方面的主体,之后会再进行阐述

自顶向下

当然,这种开发方式本质是’分析之后再开发‘,需要有职业产品经理或者业务专家,在抽象测就完成整个系统的构建,那么,这个系统将会非常稳定且高效,同时,聚合松散耦合性质还能被发挥到极致,比如应用于微服务

但是,如果面对的是非职业产品经理,或缺乏业务专家,在需求不明确的情况下进行开发,这种架构方式和普通开发没有太大效率上的差别

不过,人不可能创造出自己都不了解的东西,分析不了需求,开发工作是不可能开始的,就像你作为一个英汉传译,说话的人说的都不是英语,你怎么做这个翻译工作呢?

你可以自底向上等待机会,但是这世界没有应用是库自己组装出来的,必然有自顶向下的流程,这一部逃不过去的

很多人说 类似 redux,MVC 等分层方案,可以即刻开始开发(视图),但是如果遇到了逻辑呢?一个应用不可能不包含业务逻辑,一旦包含业务逻辑,这些方案的bad smell 便会显现出来,比如没有聚合松散耦合造成的,迭代,异步初始化困难问题(分层方案是组合套组合,即全局初始化,面向业务方案是聚合套组合)

既然了解了面向业务开发在抽象演绎阶段的理论基础,那么接下来可以好好讨论一下,如何践行面向业务开发了

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享