posts - 176, comments - 240, trackbacks - 0, articles - 7

2011年5月8日

谨以此文庆祝清华建校108周年,及5字班本科毕业20周年

作者: Canonical

众所周知,计算机科学得以存在的基石是两个基本理论:图灵于1936年提出的图灵机理论和丘奇同年早期发表的Lambda演算理论。这两个理论奠定了所谓通用计算(Universal Computation)的概念基础,描绘了具有相同计算能力(图灵完备),但形式上却南辕北辙、大相径庭的两条技术路线。如果把这两种理论看作是上帝所展示的世界本源面貌的两个极端,那么是否存在一条更加中庸灵活的到达通用计算彼岸的中间路径?

自1936年以来,软件作为计算机科学的核心应用,一直处在不间断的概念变革过程中,各类程序语言/系统架构/设计模式/方法论层出不穷,但是究其软件构造的基本原理,仍然没有脱出两个基本理论最初所设定的范围。如果定义一种新的软件构造理论,它所引入的新概念本质上能有什么特异之处?能够解决什么棘手的问题?

本文中笔者提出在图灵机和lambda演算的基础上可以很自然的引入一个新的核心概念--可逆性,从而形成一个新的软件构造理论--可逆计算(Reversible Computation)。可逆计算提供了区别于目前业内主流方法的更高层次的抽象手段,可以大幅降低软件内在的复杂性,为粗粒度软件复用扫除了理论障碍。

可逆计算的思想来源不是计算机科学本身,而是理论物理学,它将软件看作是处于不断演化过程中的抽象实体, 在不同的复杂性层次上由不同的运算规则所描述,它所关注的是演化过程中产生的微小差量如何在系统内有序的传播并发生相互作用。

本文第一节将介绍可逆计算理论的基本原理与核心公式,第二节分析可逆计算理论与组件和模型驱动等传统软件构造理论的区别和联系,并介绍可逆计算理论在软件复用领域的应用,第三节从可逆计算角度解构Docker、React等创新技术实践。

一. 可逆计算的基本原理

可逆计算可以看作是在真实的信息有限的世界中,应用图灵计算和lambda演算对世界建模的一种必然结果,我们可以通过以下简单的物理图像来理解这一点。

首先,图灵机是一种结构固化的机器,它具有可枚举的有限的状态集合,只能执行有限的几条操作指令,但是可以从无限长的纸带上读取和保存数据。例如我们日常使用的电脑,它在出厂的时候硬件功能就已经确定了,但是通过安装不同的软件,传入不同的数据文件,最终它可以自动产生任意复杂的目标输出。图灵机的计算过程在形式上可以写成

目标输出 = 固定机器(无限复杂的输入)\\

与图灵机相反的是,lambda演算的核心概念是函数,一个函数就是一台小型的计算机器,函数的复合仍然是函数,也就是说可以通过机器和机器的递归组合来产生更加复杂的机器。lambda演算的计算能力与图灵机等价,这意味着如果允许我们不断创建更加复杂的机器,即使输入一个常数0,我们也可以得到任意复杂的目标输出。lambda演算的计算过程在形式上可以写成

目标输出 = 无限复杂的机器(固定输入)\\

可以看出,以上两种计算过程都可以被表达为Y=F(X) 这样一种抽象的形式。如果我们把Y=F(X)理解为一种建模过程,即我们试图理解输入的结构以及输入和输出之间的映射关系,采用最经济的方式重建输出,则我们会发现图灵机和lambda演算都假定了现实世界中无法满足的条件。在真实的物理世界中,人类的认知总是有限的,所有的量都需要区分已知的部分和未知的部分,因此我们需要进行如下分解:

\begin{align} Y &= F(X) \\   &= (F_0+F_1)(X_0+X_1)  \tag{拆分已知和未知} \\   &= F_0(X_0)+ △      \tag{类似Taylor级数展开} \end{align}

重新整理一下符号,我们就得到了一个适应范围更加广泛的计算模式

Y=F(X)\oplus△\\

除了函数运算F(X)之外,这里出现了一个新的结构运算符⊕,它表示两个元素之间的合成运算,并不是普通数值意义上的加法,同时引出了一个新的概念:差量△。△的特异之处在于,它必然包含某种负元素,F(X)与△合并在一起之后的结果并不一定是“增加”了输出,而完全可能是“减少”。

在物理学中,差量△存在的必然性以及△包含逆元这一事实完全是不言而喻的,因为物理学的建模必须要考虑到两个基本事实:

  1. 世界是“测不准”的,噪声永远存在
  2. 模型的复杂度要和问题内在的复杂度相匹配,它捕获的是问题内核中稳定不变的趋势及规律。

例如,对以下的数据 ​ ​

我们所建立的模型只能是类似图(a)中的简单曲线,图(b)中的模型试图精确拟合每一个数据点在数学上称之为过拟合,它难以描述新的数据,而图(c)中限制差量只能为正值则会极大的限制模型的描述精度。

以上是对Y=F(X)⊕△这一抽象计算模式的一个启发式说明,下面我们将介绍在软件构造领域落实这一计算模式的一种具体技术实现方案,笔者将其命名为可逆计算。 所谓可逆计算,是指系统化的应用如下公式指导软件构造的一种技术路线

App = Biz \quad aop\_extends \quad Generator\langle DSL\rangle \\

App : 所需要构建的目标应用程序
DSL: 领域特定语言(Domain Specific Language),针对特定业务领域定制的业务逻辑描述语言,也是所谓领域模型的文本表示形式
Generator : 根据领域模型提供的信息,反复应用生成规则可以推导产生大量的衍生代码。实现方式包括独立的代码生成工具,以及基于元编程(Metaprogramming)的编译期模板展开
Biz : 根据已知模型推导生成的逻辑与目标应用程序逻辑之间的差异被识别出来,并收集在一起,构成独立的差量描述
aop_extends: 差量描述与模型生成部分通过类似面向切面编程(Aspect Oriented Programming)的技术结合在一起,这其中涉及到对模型生成部分的增加、修改、替换、删除等一系列操作

DSL是对关键性领域信息的一种高密度的表达,它直接指导Generator生成代码,这一点类似于图灵计算通过输入数据驱动机器执行内置指令。而如果把Generator看作是文本符号的替换生成,则它的执行和复合规则完全就是lambda演算的翻版。差量合并在某种意义上是一种很新奇的操作,因为它要求我们具有一种细致入微、无所不达的变化收集能力,能够把散布系统各处的同阶小量分离出来并合并在一起,这样差量才具有独立存在的意义和价值。同时,系统中必须明确建立逆元和逆运算的概念,在这样的概念体系下差量作为“存在”与“不存在”的混合体才可能得到表达。

现有的软件基础架构如果不经过彻底的改造,是无法有效的实施可逆计算的。正如图灵机模型孕育了C语言,Lambda演算促生了Lisp语言一样,为了有效支持可逆计算,笔者提出了一种新的程序语言X语言,它内置了差量定义、生成、合并、拆分等关键特性,可以快速建立领域模型,并在领域模型的基础上实现可逆计算。

为了实施可逆计算,我们必须要建立差量的概念。变化产生差量,差量有正有负,而且应该满足下面三条要求:

  1. 差量独立存在
  2. 差量相互作用
  3. 差量具有结构

在第三节中笔者将会以Docker为实例说明这三条要求的重要性。

可逆计算的核心是“可逆”,这一概念与物理学中熵的概念息息相关,它的重要性其实远远超出了程序构造本身,在可逆计算的方法论来源一文中,笔者会对它有更详细的阐述。

正如复数的出现扩充了代数方程的求解空间,可逆计算为现有的软件构造技术体系补充了“可逆的差量合并”这一关键性技术手段,从而极大扩充了软件复用的可行范围,使得系统级的粗粒度软件复用成为可能。同时在新的视角下,很多原先难以解决的模型抽象问题可以找到更加简单的解决方案,从而大幅降低了软件构造的内在复杂性。在第二节中笔者将会对此进行详细阐述。

软件开发虽然号称是知识密集性的工作,但到目前为止,众多一线程序员的日常中仍然包含着大量代码拷贝/粘贴/修改的机械化手工操作内容,而在可逆计算理论中,代码结构的修改被抽象为可自动执行的差量合并规则,因此通过可逆计算,我们可以为软件自身的自动化生产创造基础条件。笔者在可逆计算理论的基础上,提出了一个新的软件工业化生产模式NOP(Nop is nOt Programming),以非编程的方式批量生产软件。NOP不是编程,但也不是不编程,它强调的是将业务人员可以直观理解的逻辑与纯技术实现层面的逻辑相分离,分别使用合适的语言和工具去设计,然后再把它们无缝的粘接在一起。笔者将在另一篇文章中对NOP进行详细介绍。

可逆计算与可逆计算机有着同样的物理学思想来源,虽然具体的技术内涵并不一致,但它们目标却是统一的。正如云计算试图实现计算的云化一样,可逆计算和可逆计算机试图实现的都是计算的可逆化。

二. 可逆计算对传统理论的继承和发展

(一)组件(Component)

软件的诞生源于数学家研究希尔伯特第十问题时的副产品,早期软件的主要用途也是数学物理计算,那时软件中的概念无疑是抽象的、数学化的。随着软件的普及,越来越多应用软件的研发催生了面向对象和组件化的方法论,它试图弱化抽象思维,转而贴近人类的常识,从人们的日常经验中汲取知识,把业务领域中人们可以直观感知的概念映射为软件中的对象,仿照物质世界的生产制造过程从无到有、从小到大,逐步拼接组装实现最终软件产品的构造。

像框架、组件、设计模式、架构视图等软件开发领域中耳熟能详的概念,均直接来自于建筑业的生产经验。组件理论继承了面向对象思想的精华,借助可复用的预制构件这一概念,创造了庞大的第三方组件市场,获得了空前的技术和商业成功,即使到今天仍然是最主流的软件开发指导思想。但是,组件理论内部存在着一个本质性的缺陷,阻碍了它把自己的成功继续推进到一个新的高度。

我们知道,所谓复用就是对已有的制成品的重复使用。为了实现组件复用,我们需要找到两个软件中的公共部分,把它分离出来并按照组件规范整理成标准形式。但是,A和B的公共部分的粒度是比A和B都要小的,大量软件的公共部分是比它们中任何一个的粒度都要小得多的。这一限制直接导致越大粒度的软件功能模块越难以被直接复用,组件复用存在理论上的极限。可以通过组件组装复用60%-70%的工作量,但是很少有人能超过80%,更不用说实现复用度90%以上的系统级整体复用了。

为了克服组件理论的局限,我们需要重新认识软件的抽象本质。软件是在抽象的逻辑世界中存在的一种信息产品,信息并不是物质。抽象世界的构造和生产规律与物质世界是有着本质不同的。物质产品的生产总是有成本的,而复制软件的边际成本却可以是0。将桌子从房间中移走在物质世界中必须要经过门或窗,但在抽象的信息空间中却只需要将桌子的坐标从x改为-x而已。抽象元素之间的运算关系并不受众多物理约束的限制,因此信息空间中最有效的生产方式不是组装,而是掌握和制定运算规则。

如果从数学的角度重新去解读面向对象和组件技术,我们会发现可逆计算可以被看作是组件理论的一个自然扩展。

  • 面向对象 : 不等式 A > B
  • 组件 : 加法 A = B + C
  • 可逆计算 : 差量 Y = X + △Y

面向对象中的一个核心概念是继承:派生类从基类继承,自动具有基类的一切功能。例如老虎是动物的一种派生类,在数学上,我们可以说老虎(A)这个概念所包含的内容比动物(B)这个概念更多,老虎>动物(即A>B)。据此我们可以知道,动物这个概念所满足的命题,老虎自然满足, 例如动物会奔跑,老虎必然也会奔跑( P(B) -> P(A) )。程序中所有用到动物这一概念的地方都可以被替换为老虎(Liscov代换原则)。这样通过继承就将自动推理关系引入到软件领域中来,在数学上这对应于不等式,也就是一种偏序关系。

面向对象的理论困境在于不等式的表达能力有限。对于不等式A > B,我们知道A比B多,但是具体多什么,我们并没有办法明确的表达出来。而对于 A > B, D > E这样的情况,即使多出来的部分一模一样,我们也无法实现这部分内容的重用。组件技术明确指出"组合优于继承",这相当于引入了加法

A=B+C\\ D=E+C

这样就可以抽象出组件C进行重用。

沿着上述方向推演下去,我们很容易确定下一步的发展是引入“减法”,这样才可以把 A = B + C看作是一个真正的方程,通过方程左右移项求解出

B=A-C=A+(-C)\\

通过减法引入的“负组件”是一个全新的概念,它为软件复用打开了一扇新的大门。

假设我们已经构建好了系统 X = D + E + F, 现在需要构建 Y = D + E + G。如果遵循组件的解决方案,则需要将X拆解为多个组件,然后更换组件F为G后重新组装。而如果遵循可逆计算的技术路线,通过引入逆元 -F, 我们立刻得到

Y = X-F+G=X+(-F+G)=X+△Y\\

在不拆解X的情况下,通过直接追加一个差量△Y,即可将系统X转化为系统Y。

组件的复用条件是“相同方可复用”,但在存在逆元的情况下,具有最大颗粒度的完整系统X在完全不改的情况下直接就可以被复用,软件复用的范围被拓展为“相关即可复用”,软件复用的粒度不再有任何限制。组件之间的关系也发生了深刻的变化,不再是单调的构成关系,而成为更加丰富多变的转化关系

Y = X + △Y 这一物理图像对于复杂软件产品的研发具有非常现实的意义。X可以是我们所研发的软件产品的基础版本或者说主版本,在不同的客户处部署实施时,大量的定制化需求被隔离到独立的差量△Y中,这些定制的差量描述单独存放,通过编译技术与主版本代码再合并到一起。主版本的架构设计和代码实现只需要考虑业务领域内稳定的核心需求,不会受到特定客户处偶然性需求的冲击,从而有效的避免架构腐化。主版本研发和多个项目的实施可以并行进行,不同的实施版本对应不同的△Y,互不影响,同时主版本的代码与所有定制代码相互独立,能够随时进行整体升级。

(二)模型驱动架构(Model Driven Architecture)

模型驱动架构(MDA)是由对象管理组织(Object Management Group,OMG)在2001年提出的软件架构设计和开发方法,它被看作是软件开发模式从以代码为中心向以模型为中心转变的里程碑,目前大部分所谓软件开发平台的理论基础都与MDA有关。

MDA试图提升软件开发的抽象层次,直接使用建模语言(例如Executable UML)作为编程语言,然后通过使用类似编译器的技术将高层模型翻译为底层的可执行代码。在MDA中,明确区分应用架构和系统架构,并分别用平台无关模型PIM(Platform Independent Model)和平台相关模型PSM(Platform Specific Model)来描述它们。PIM反映了应用系统的功能模型,它独立于具体的实现技术和运行框架,而PSM则关注于使用特定技术(例如J2EE或者dotNet)实现PIM所描述的功能,为PIM提供运行环境。

使用MDA的理想场景是,开发人员使用可视化工具设计PIM,然后选择目标运行平台,由工具自动执行针对特定平台和实现语言的映射规则,将PIM转换为对应的PSM,并最终生成可执行的应用程序代码。基于MDA的程序构造可以表述为如下公式

App=Transformer(PIM)\\

MDA的愿景是像C语言取代汇编那样最终彻底消灭传统编程语言。但经历了这么多年发展之后,它仍未能够在广泛的应用领域中展现出相对于传统编程压倒性的竞争优势。

事实上,目前基于MDA的开发工具在面对多变的业务领域时,总是难掩其内在的不适应性。根据本文第一节的分析,我们知道建模必须要考虑差量。而在MDA的构造公式中,左侧的App代表了各种未知需求,而右侧的Transformer和PIM的设计器实际上都主要由开发工具厂商提供,未知=已知这样一个方程是无法持久保持平衡的。

目前,工具厂商的主要做法是提供大而全的模型集合,试图事先预测用户所有可能的业务场景。但是,我们知道“天下没有免费的午餐”,模型的价值在于体现了业务领域中的本质性约束,没有任何一个模型是所有场景下都最优的。预测需求会导致出现一种悖论: 模型内置假定过少,则无法根据用户输入的少量信息自动生成大量有用的工作,也无法防止用户出现误操作,模型的价值不明显,而如果反之,模型假定很多,则它就会固化到某个特定业务场景,难以适应新的情况。

打开一个MDA工具的设计器,我们最经常的感受是大部分选项都不需要,也不知道是干什么用的,需要的选项却到处找也找不到。

可逆计算对MDA的扩展体现为两点:

  1. 可逆计算中Generator和DSL都是鼓励用户扩充和调整的,这一点类似于面向语言编程(Language-oriented programming)
  2. 存在一个额外的差量定制机会,可以对整体生成结果进行精确的局部修正。

在笔者提出的NOP生产模式中,必须要包含一个新的关键组件:设计器的设计器。普通的程序员可以利用设计器的设计器快速设计开发自己的领域特定语言(DSL)及其可视化设计器,同时可以通过设计器的设计器对系统中的任意设计器进行定制调整,自由的增加或者删除元素。

(三)面向切面编程(Aspect Oriented Programming)

面向切面(AOP)是与面向对象(OOP)互补的一种编程范式,它可以实现对那些横跨多个对象的所谓横切关注点(cross-cutting concern)的封装。例如,需求规格中可能规定所有的业务操作都要记录日志,所有的数据库修改操作都要开启事务。如果按照面向对象的传统实现方式,需求中的一句话将会导致众多对象类中陡然膨胀出现大量的冗余代码,而通过AOP, 这些公共的“修饰性”的操作就可以被剥离到独立的切面描述中。这就是所谓纵向分解和横向分解的正交性。


AOP本质上是两个能力的组合:

  1. 在程序结构空间中定位到目标切点(Pointcut)
  2. 对局部程序结构进行修改,将扩展逻辑(Advice)编织(Weave)到指定位置。

定位依赖于存在良好定义的整体结构坐标系(没有坐标怎么定位?),而修改依赖于存在良好定义的局部程序语义结构。目前主流的AOP技术的局限性在于,它们都是在面向对象的语境下表达的,而领域结构与对象实现结构并不总是一致的,或者说用对象体系的坐标去表达领域语义是不充分的。例如,申请人和审批人在领域模型中是需要明确区分的不同的概念,但是在对象层面却可能都对应于同样的Person类,使用AOP的很多时候并不能直接将领域描述转换为切点定义和Advice实现。这种限制反映到应用层面,结果就是除了日志、事务、延迟加载、缓存等少数与特定业务领域无关的“经典”应用之外,我们找不到AOP的用武之地。

可逆计算需要类似AOP的定位和结构修正能力,但是它是在领域模型空间中定义这些能力的,因而大大扩充了AOP的应用范围。特别是,可逆计算中领域模型自我演化产生的结构差量△能够以类似AOP切面的形式得到表达。

我们知道,组件可以标识出程序中反复出现的“相同性”,而可逆计算可以捕获程序结构的“相似性”。相同很罕见,需要敏锐的甄别,但是在任何系统中,有一种相似性都是唾手可得的,即动力学演化过程中系统与自身历史快照之间的相似性。这种相似性在此前的技术体系中并没有专门的技术表达形式。

通过纵向和横向分解,我们所建立的概念之网存在于一个设计平面当中,当设计平面沿着时间轴演化时,很自然的会产生一个“三维”映射关系:后一时刻的设计平面可以看作是从前一时刻的平面增加一个差量映射(定制)而得到,而差量是定义在平面的每一个点上的。这一图像类似于范畴论(Category Theory)中的函子(Functor)概念,可逆计算中的差量合并扮演了函子映射的角色。因此,可逆计算相当于扩展了原有的设计空间,为演化这一概念找到了具体的一种技术表现形式。

(四)软件产品线(Software Product Line)

软件产品线理论源于一个洞察,即在一个业务领域中,很少有软件系统是完全独特的,大量的软件产品之间存在着形式和功能的相似性,可以归结为一个产品家族,把一个产品家族中的所有产品(已存在的和尚未存在的)作为一个整体来研究、开发、演进,通过科学的方法提取它们的共性,结合有效的可变性管理,就有可能实现规模化、系统化的软件复用,进而实现软件产品的工业化生产。

软件产品线工程采用两阶段生命周期模型,区分领域工程应用工程。所谓领域工程,是指分析业务领域内软件产品的共性,建立领域模型及公共的软件产品线架构,形成可复用的核心资产的过程,即面向复用的开发(development for reuse)。而应用工程,其实质是使用复用来开发( development with reuse),也就是利用已经存在的体系架构、需求、测试、文档等核心资产来制造具体应用产品的生产活动。

卡耐基梅隆大学软件工程研究所(CMU-SEI)的研究人员在2008年的报告中宣称软件产品线可以带来如下好处:

  1. 提升10倍以上生产率
  2. 提升10倍以上产品质量
  3. 缩减60%以上成本
  4. 缩减87%以上人力需求
  5. 缩减98%以上产品上市时间
  6. 进入新市场的时间以月计,而不是年

软件产品线描绘的理想非常美好:复用度90%以上的产品级复用、随需而变的敏捷定制、无视技术变迁影响的领域架构、优异可观的经济效益等等。它所存在的唯一问题就是如何才能做到?尽管软件产品线工程试图通过综合利用所有管理的和技术的手段,在组织级别策略性的复用一切技术资产(包括文档、代码、规范、工具等等),但在目前主流的技术体制下,发展成功的软件产品线仍然面临着重重困难。

可逆计算的理念与软件产品线理论高度契合,它的技术方案为软件产品线的核心技术困难---可变性管理带来了新的解决思路。在软件产品线工程中,传统的可变性管理主要是适配、替换和扩展这三种方式:


这三种方式都可以看作是向核心架构补充功能。但是可复用性的障碍不仅仅是来自于无法追加新的功能,很多时候也在于无法屏蔽原先已经存在的功能。传统的适配技术等要求接口一致匹配,是一种刚性的对接要求,一旦失配必将导致不断向上传导应力,最终只能通过整体更换组件来解决问题。可逆计算通过差量合并为可变性管理补充了“消除”这一关键性机制,可以按需在领域模型空间中构建出柔性适配接口,从而有效的控制变化点影响范围。

可逆计算中的差量虽然也可以被解释为对基础模型的一种扩展,但是它与插件扩展技术之间还是存在着明显的区别。在平台-插件这样的结构中,平台是最核心的主体,插件依附于平台而存在,更像是一种补丁机制,在概念层面上是相对次要的部分。而在可逆计算中,通过一些形式上的变换,我们可以得到一个对称性更高的公式:

 A = B \oplus G(D) \equiv (B,D) \\

如果把G看作是一种相对不变的背景知识,则形式上我们可以把它隐藏起来,定义一个更加高级的“括号”运算符,它类似于数学中的“内积”。在这种形式下,B和D是对偶的,B是对D的补充,而D也是对B的补充。同时,我们注意到G(D)是模型驱动架构的体现,模型驱动之所以有价值就在于模型D中发生的微小变化,可以被G放大为系统各处大量衍生的变化,因此G(D)是一种非线性变换,而B是系统中去除D所对应的非线性因素之后剩余的部分。当所有复杂的非线性影响因素都被剥离出去之后,最后剩下的部分B就有可能是简单的,甚至能够形成一种新的可独立理解的领域模型结构(可以类比声波与空气的关系,声波是空气的扰动,但是不用研究空气本体,我们就可以直接用正弦波模型来描述声波)。

A = (B,D)的形式可以直接推广到存在更多领域模型的情况

 A = (B,D,E,F, ...) \\

因为B、D、E等概念都是某种DSL所描述的领域模型,因此它们可以被解释为A投影到特定领域模型子空间所产生的分量,也就是说,应用A可以被表示为一个“特征向量”(Feature Vector), 例如

应用A = (Biz,权限,流程,报表,...) \\

与软件产品线中常用的面向特征编程(Feature Oriented Programming)相比,可逆计算的特征分解方案强调领域特定描述,特征边界更加明确,特征合成时产生的概念冲突更容易处理。

特征向量本身构成更高维度的领域模型,它可以被进一步分解下去,从而形成一个模型级列,例如定义

D' \equiv [B,D]\ G'(D')\equiv B\oplus G(D)\\

, 并且假设D'可以继续分解

D'=V\oplus M(U)=M'(U') \\

,则可以得到

\begin{align} A &= B ⊕ G(D) \\   &= G'(D') \\  &= G'(M'(U'))\\  &= G'(M'([V,U])) \\  \end{align} \\

最终我们可以通过领域特征向量U'来描述D’,然后再通过领域特征向量D‘来描述原有的模型A。

可逆计算的这一构造策略类似于深度神经网络,它不再局限于具有极多可调参数的单一模型,而是建立抽象层级不同、复杂性层级不同的一系列模型,通过逐步求精的方式构造出最终的应用。

在可逆计算的视角下,应用工程的工作内容变成了使用特征向量来描述软件需求,而领域工程则负责根据特征向量描述来生成最终的软件。

三. 初露端倪的差量革命

(一)Docker

Docker是2013年由创业公司dotCloud开源的应用容器引擎,它可以将任何应用及其依赖的环境打包成一个轻量级、可移植、自包含的容器(Container),并据此以容器为标准化单元创造了一种新型的软件开发、部署和交付形式。

Docker一出世就秒杀了Google的亲儿子lmctfy (Let Me Contain That For You)容器技术,同时也把Google的另一个亲儿子Go语言迅速捧成了网红,之后Docker的发展 更是一发而不可收拾。2014年开始一场Docker风暴席卷全球,以前所未有的力度推动了操作系统内核的变革,在众多巨头的跟风造势下瞬间引爆容器云市场,真正从根本上改变了企业应用从开发、构建到部署、运行整个生命周期的技术形态。

Docker技术的成功源于它对软件运行时复杂性的本质性降低,而它的技术方案可以看作是可逆计算理论的一种特例。Docker的核心技术模式可以用如下公式进行概括

App = Docker\langle Dockerfile\rangle \quad unionfs \quad BaseImage\\

Dockerfile是构建容器镜像的一种领域特定语言,例如

FROM ubuntu:16.04 
RUN useradd --user-group --create-home --shell /bin/bash work
RUN apt-get update -y && apt-get install -y python3-dev
COPY . /app RUN make /app
ENV PYTHONPATH /FrameworkBenchmarks
CMD python /app/app.py
EXPOSE 8088

通过Dockerfile可以快速准确的描述容器所依赖的基础镜像,具体的构建步骤,运行时环境变量和系统配置等信息。

Docker应用程序扮演了可逆计算中Generator的角色,负责解释Dockerfile,执行对应的指令来生成容器镜像。

创造性的使用联合文件系统(Union FS),是Docker的一个特别的创新之处。这种文件系统采用分层的构造方式,每一层构建完毕后就不会再发生改变,在后一层上进行的任何修改都只会记录在自己这一层。例如,修改前一层的文件时会通过Copy-On-Write的方式复制一份到当前层,而删除前一层的文件并不会真的执行删除操作,而是仅在当前层标记该文件已删除。Docker利用联合文件系统来实现将多个容器镜像合成为一个完整的应用,这一技术的本质正是可逆计算中的aop_extends操作。

Docker的英文是码头搬运工人的意思,它所搬运的容器也经常被人拿来和集装箱做对比:标准的容器和集装箱类似,使得我们可以自由的对它们进行传输/组合,而不用考虑容器中的具体内容。但是这种比较是肤浅的,甚至是误导性的。集装箱是静态的、简单的、没有对外接口的,而容器则是动态的、复杂的、和外部存在着大量信息交互的。这种动态的复杂结构想和普通的静态物件一样封装成所谓标准容器,其难度不可同日而语。如果没有引入支持差量的文件系统,是无法构建出一种柔性边界,实现逻辑分离的。

Docker所做的标准封装其实虚拟机也能做到,甚至差量存储机制在虚拟机中也早早的被用于实现增量备份,Docker与虚拟机的本质性不同到底在什么地方?回顾第一节中可逆计算对差量三个基本要求,我们可以清晰的发现Docker的独特之处。

  1. 差量独立存在:Docker最重要的价值就在于通过容器封装,抛弃了作为背景存在(必不可少,但一般情况下不需要了解),占据了99%的体积和复杂度的操作系统层。应用容器成为了可以独立存储、独立操作的第一性的实体。轻装上阵的容器在性能、资源占用、可管理性等方面完全超越了虚胖的虚拟机。
  2. 差量相互作用:Docker容器之间通过精确受控的方式发生相互作用,可通过操作系统的namespace机制选择性的实现资源隔离或者共享。而虚拟机的差量切片之间是没有任何隔离机制的。
  3. 差量具有结构:虚拟机虽然支持增量备份,但是人们却没有合适的手段去主动构造一个指定的差量切片出来。归根结底,是因为虚拟机的差量定义在二进制字节空间中,而这个空间非常贫瘠,几乎没有什么用户可以控制的构造模式。而Docker的差量是定义在差量文件系统空间中,这个空间继承了Linux社区最丰富的历史资源。每一条shell指令的执行结果最终反映到文件系统中都是增加/删除/修改了某些文件,所以每一条shell指令都可以被看作是某个差量的定义。差量构成了一个异常丰富的结构空间,差量既是这个空间中的变换算符(shell指令),又是变换算符的运算结果。差量与差量相遇产生新的差量,这种生生不息才是Docker的生命力所在。

(二)React

2013年,也就是Docker发布的同一年,Facebook公司开源了一个革命性的Web前端框架React。React的技术思想非常独特,它以函数式编程思想为基础,结合一个看似异想天开的虚拟DOM(Virtual DOM)概念,引入了一整套新的设计模式,开启了前端开发的新大航海时代。

class HelloMessage extends React.Component {
  constructor(props) {
    super(props);
    
this.state = { count: 0 };
    
this.action = this.action.bind(this);
  }
  
  action(){
    
this.setState(state => ({
      count: state.count 
+ 1
    }));
  },
  
  render() {
    
return (
      
<button onClick={this.action}>
        Hello {
this.props.name}:{this.state.count}
      
</button>
    );
  }
}

ReactDOM.render(
  
<HelloMessage name="Taylor" />,
  mountNode
);

React组件的核心是render函数,它的设计参考了后端常见的模板渲染技术,主要区别在于后端模板输出的是HTML文本,而React组件的Render函数使用类似XML模板的JSX语法,通过编译转换在运行时输出的是虚拟DOM节点对象。例如上面HelloMessage组件的render函数被翻译后的结果类似于

render(){
   
return new VNode("button", {onClick: this.action, 
          content: 
"Hello "+ this.props.name + ":" + this.state.count });
}
可以用以下公式来描述React组件:  VDom = render(state)
当状态发生变化以后,只要重新执行render函数就会生成新的虚拟DOM节点,虚拟DOM节点可以被翻译成真实的HTML DOM对象,从而实现界面更新。这种根据状态重新生成完整视图的渲染策略极大简化了前端界面开发。例如对于一个列表界面,传统编程需要编写新增行/更新行/删除行等多个不同的DOM操作函数,而在React中只要更改state后重新执行唯一的render函数即可。

每次重新生成DOM视图的唯一问题是性能很低,特别是当前端交互操作众多、状态变化频繁的时候。React的神来之笔是提出了基于虚拟DOM的diff算法,可以自动的计算两个虚拟DOM树之间的差量,状态变化时只要执行虚拟Dom差量对应的DOM修改操作即可(更新真实DOM时会触发样式计算和布局计算,导致性能很低,而在JavaScript中操作虚拟DOM 的速度是非常快的)。整体策略可以表示为如下公式

state_1=state_0\oplus action\\ \Delta VDom=render(state_1)-render(state_0)\\ \Delta Dom =Translater(\Delta VDom) \\

显然,这一策略也是可逆计算的一种特例。

只要稍微留意一下就会发现,最近几年merge/diff/residual/delta等表达差量运算的概念越来越多的出现在软件设计领域中。比如大数据领域的流计算引擎中,流与表之间的关系可以表示为

Table=\int \Delta Stream \\

对表的增删改查操作可以被编码为事件流,而将表示数据变化的事件累积到一起就形成了数据表。

现代科学发端于微积分的发明,而微分的本质就是自动计算无穷小差量,而积分则是微分的逆运算,自动对无穷小量进行汇总合并。19世纪70年代,经济学经历了一场边际革命,将微积分的思想引入经济分析,在边际这一概念之上重建了整个经济学大厦。软件构造理论发展到今天,已经进入一个瓶颈,也到了应该重新认识差量的时候。

四. 结语

笔者的专业背景是理论物理学,可逆计算源于笔者将物理学和数学的思想引入软件领域的一种尝试,它最早由笔者在2007年左右提出。一直以来,软件领域对于自然规律的应用一般情况下都只限于"模拟"范畴,例如流体动力学模拟软件,虽然它内置了人类所认知的最深刻的一些世界规律,但这些规律并没有被用于指导和定义软件世界自身的构造和演化,它们的指向范围是软件世界之外,而不是软件世界自身。在笔者看来,在软件世界中,我们完全可以站在“上帝的视角”,规划和定义一系列的结构构造规律,辅助我们完成软件世界的构建。而为了完成这一点,我们首先需要建立程序世界中的“微积分”。

类似于微积分,可逆计算理论的核心是将“差量”提升为第一性的概念,将全量看作是差量的一种特例(全量=单位元+全量)。传统的程序世界中我们所表达的都只是“有”,而且是“所有”,差量只能通过全量之间的运算间接得到,它的表述和操纵都需要特殊处理,而基于可逆计算理论,我们首先应该定义所有差量概念的表达形式,然后再围绕这些概念去建立整个领域概念体系。为了保证差量所在数学空间的完备性(差量之间的运算结果仍然需要是合法的差量),差量所表达的不能仅仅是“有”,而必须是“有”和“没有”的一种混合体。也就是说差量必须是“可逆的”。可逆性具有非常深刻的物理学内涵,在基本的概念体系中内置这一概念可以解决很多非常棘手的软件构造问题。

为了处理分布式问题,现代软件开发体系已经接受了不可变数据的概念,而为了解决大粒度软件复用问题,我们还需要接受不可变逻辑的概念(复用可以看作是保持原有逻辑不变,然后增加差量描述)。目前,业内已经逐步出现了一些富有创造性的主动应用差量概念的实践,它们都可以在可逆计算的理论框架下得到统一的诠释。笔者提出了一种新的程序语言X语言,它可以极大简化可逆计算的技术实现。目前笔者已经基于X语言设计并实现了一系列软件框架和生产工具,并基于它们提出了一种新的软件生产范式(NOP)。

posted @ 2019-04-29 10:18 canonical 阅读(700) | 评论 (0)编辑 收藏

  浏览器前端编程的面貌自2005年以来已经发生了深刻的变化,这并不简单的意味着出现了大量功能丰富的基础库,使得我们可以更加方便的编写业务代码,更重要的是我们看待前端技术的观念发生了重大转变,明确意识到了如何以前端特有的方式释放程序员的生产力。本文将结合jQuery源码的实现原理,对javascript中涌现出的编程范式和常用技巧作一简单介绍。
 
1. AJAX: 状态驻留,异步更新
  首先来看一点历史。
A. 1995年Netscape公司的Brendan Eich开发了javacript语言,这是一种动态(dynamic)、弱类型(weakly typed)、基于原型(prototype-based)的脚本语言。
B. 1999年微软IE5发布,其中包含了XMLHTTP ActiveX控件。
C. 2001年微软IE6发布,部分支持DOM level 1和CSS 2标准。
D. 2002年Douglas Crockford发明JSON格式。
至此,可以说Web2.0所依赖的技术元素已经基本成形,但是并没有立刻在整个业界产生重大的影响。尽管一些“页面异步局部刷新”的技巧在程序员中间秘密的流传,甚至催生了bindows这样庞大臃肿的类库,但总的来说,前端被看作是贫瘠而又肮脏的沼泽地,只有后台技术才是王道。到底还缺少些什么呢?
  当我们站在今天的角度去回顾2005年之前的js代码,包括那些当时的牛人所写的代码,可以明显的感受到它们在程序控制力上的孱弱。并不是说2005年之前的js技术本身存在问题,只是它们在概念层面上是一盘散沙,缺乏统一的观念,或者说缺少自己独特的风格, 自己的灵魂。当时大多数的人,大多数的技术都试图在模拟传统的面向对象语言,利用传统的面向对象技术,去实现传统的GUI模型的仿制品。
  2005年是变革的一年,也是创造概念的一年。伴随着Google一系列让人耳目一新的交互式应用的发布,Jesse James Garrett的一篇文章《Ajax: A New Approach to Web Applications》被广为传播。Ajax这一前端特有的概念迅速将众多分散的实践统一在同一口号之下,引发了Web编程范式的转换。所谓名不正则言不顺,这下无名群众可找到组织了。在未有Ajax之前,人们早已认识到了B/S架构的本质特征在于浏览器和服务器的状态空间是分离的,但是一般的解决方案都是隐藏这一区分,将前台状态同步到后台,由后台统一进行逻辑处理,例如ASP.NET。因为缺乏成熟的设计模式支持前台状态驻留,在换页的时候,已经装载的js对象将被迫被丢弃,这样谁还能指望它去完成什么复杂的工作吗?
  Ajax明确提出界面是局部刷新的,前台驻留了状态,这就促成了一种需要:需要js对象在前台存在更长的时间。这也就意味着需要将这些对象和功能有效的管理起来,意味着更复杂的代码组织技术,意味着对模块化,对公共代码基的渴求。
  jQuery现有的代码中真正与Ajax相关(使用XMLHTTP控件异步访问后台返回数据)的部分其实很少,但是如果没有Ajax, jQuery作为公共代码基也就缺乏存在的理由。

2. 模块化:管理名字空间
  当大量的代码产生出来以后,我们所需要的最基础的概念就是模块化,也就是对工作进行分解和复用。工作得以分解的关键在于各人独立工作的成果可以集成在一起。这意味着各个模块必须基于一致的底层概念,可以实现交互,也就是说应该基于一套公共代码基,屏蔽底层浏览器的不一致性,并实现统一的抽象层,例如统一的事件管理机制等。比统一代码基更重要的是,各个模块之间必须没有名字冲突。否则,即使两个模块之间没有任何交互,也无法共同工作。
  jQuery目前鼓吹的主要卖点之一就是对名字空间的良好控制。这甚至比提供更多更完善的功能点都重要的多。良好的模块化允许我们复用任何来源的代码,所有人的工作得以积累叠加。而功能实现仅仅是一时的工作量的问题。jQuery使用module pattern的一个变种来减少对全局名字空间的影响,仅仅在window对象上增加了一个jQuery对象(也就是$函数)。
   所谓的module pattern代码如下,它的关键是利用匿名函数限制临时变量的作用域。
  var feature =(function() {

// 私有变量和函数
var privateThing = 'secret',
    publicThing = 'not secret',

    changePrivateThing = function() {
        privateThing = 'super secret';
    },

    sayPrivateThing = function() {
        console.log(privateThing);
        changePrivateThing();
    };

// 返回对外公开的API
return {
    publicThing : publicThing,
    sayPrivateThing :  sayPrivateThing
}
})();
  js本身缺乏包结构,不过经过多年的尝试之后业内已经逐渐统一了对包加载的认识,形成了RequireJs库这样得到一定共识的解决方案。jQuery可以与RequireJS库良好的集成在一起, 实现更完善的模块依赖管理。http://requirejs.org/docs/jquery.html
 
  require(["jquery", "jquery.my"], function() {
    //当jquery.js和jquery.my.js都成功装载之后执行
    $(function(){
      $('#my').myFunc();
    });
  });
 
  通过以下函数调用来定义模块my/shirt, 它依赖于my/cart和my/inventory模块,
  require.def("my/shirt",
    ["my/cart", "my/inventory"],
    function(cart, inventory) {
        // 这里使用module pattern来返回my/shirt模块对外暴露的API
        return {
            color: "blue",
            size: "large"
            addToCart: function() {
                // decrement是my/inventory对外暴露的API
                inventory.decrement(this);
                cart.add(this);
            }
        }
    }
  );

3. 神奇的$:对象提升
  当你第一眼看到$函数的时候,你想到了什么?传统的编程理论总是告诉我们函数命名应该准确,应该清晰无误的表达作者的意图,甚至声称长名字要优于短名字,因为减少了出现歧义的可能性。但是,$是什么?乱码?它所传递的信息实在是太隐晦,太暧昧了。$是由prototype.js库发明的,它真的是一个神奇的函数,因为它可以将一个原始的DOM节点提升(enhance)为一个具有复杂行为的对象。在prototype.js最初的实现中,$函数的定义为
  var $ = function (id) {
    return "string" == typeof id ? document.getElementById(id) : id;
  };
  这基本对应于如下公式
      e = $(id)
  这绝不仅仅是提供了一个聪明的函数名称缩写,更重要的是在概念层面上建立了文本id与DOM element之间的一一对应。在未有$之前,id与对应的element之间的距离十分遥远,一般要将element缓存到变量中,例如
  var ea = docuement.getElementById('a');
  var eb = docuement.getElementById('b');
  ea.style....
但是使用$之后,却随处可见如下的写法
  $('header_'+id).style...
  $('body_'+id)....
id与element之间的距离似乎被消除了,可以非常紧密的交织在一起。
  prototype.js后来扩展了$的含义,
  function $() {
    var elements = new Array();
    
    for (var i = 0; i < arguments.length; i++) {
        var element = arguments[i];
        if (typeof element == 'string')
          element = document.getElementById(element);
    
        if (arguments.length == 1)
          return element;
    
        elements.push(element);
    }
    
    return elements;
  }
  这对应于公式
    [e,e] = $(id,id)
  很遗憾,这一步prototype.js走偏了,这一做法很少有实用的价值。
  真正将$发扬光大的是jQuery, 它的$对应于公式
    [o] = $(selector)
  这里有三个增强
  A. selector不再是单一的节点定位符,而是复杂的集合选择符
  B. 返回的元素不是原始的DOM节点,而是经过jQuery进一步增强的具有丰富行为的对象,可以启动复杂的函数调用链。
  C. $返回的包装对象被造型为数组形式,将集合操作自然的整合到调用链中。

  当然,以上仅仅是对神奇的$的一个过分简化的描述,它的实际功能要复杂得多. 特别是有一个非常常用的直接构造功能.
   $("<table><tbody><tr><td>...</td></tr></tbody></table>")....
  jQuery将根据传入的html文本直接构造出一系列的DOM节点,并将其包装为jQuery对象. 这在某种程度上可以看作是对selector的扩展: html内容描述本身就是一种唯一指定.
  $(function{})这一功能就实在是让人有些无语了, 它表示当document.ready的时候调用此回调函数。真的,$是一个神奇的函数, 有任何问题,请$一下。
  总结起来, $是从普通的DOM和文本描述世界到具有丰富对象行为的jQuery世界的跃迁通道。跨过了这道门,就来到了理想国。
   
4. 无定形的参数:专注表达而不是约束
  弱类型语言既然头上顶着个"弱"字, 总难免让人有些先天不足的感觉. 在程序中缺乏类型约束, 是否真的是一种重大的缺憾? 在传统的强类型语言中, 函数参数的类型,个数等都是由编译器负责检查的约束条件, 但这些约束仍然是远远不够的. 一般应用程序中为了加强约束, 总会增加大量防御性代码, 例如在C++中我们常用ASSERT, 而在java中也经常需要判断参数值的范围
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException(
            "Index: "+index+", Size: "+size);            
  很显然, 这些代码将导致程序中存在大量无功能的执行路径, 即我们做了大量判断, 代码执行到某个点, 系统抛出异常, 大喊此路不通. 如果我们换一个思路, 既然已经做了某种判断,能否利用这些判断的结果来做些什么呢? javascript是一种弱类型的语言,它是无法自动约束参数类型的, 那如果顺势而行,进一步弱化参数的形态, 将"弱"推进到一种极致, 在弱无可弱的时候, weak会不会成为标志性的特点?
  看一下jQuery中的事件绑定函数bind,
   A. 一次绑定一个事件 $("#my").bind("mouseover", function(){});
   B. 一次绑定多个事件 $("#my").bind("mouseover mouseout",function(){})
   C. 换一个形式, 同样绑定多个事件
      $("#my").bind({mouseover:function(){}, mouseout:function(){});
   D. 想给事件监听器传点参数
      $('#my').bind('click', {foo: "xxxx"}, function(event) { event.data.foo..})
   E. 想给事件监听器分个组
      $("#my").bind("click.myGroup″, function(){});
   F. 这个函数为什么还没有疯掉???
   
   就算是类型不确定, 在固定位置上的参数的意义总要是确定的吧? 退一万步来说, 就算是参数位置不重要了,函数本身的意义应该是确定的吧? 但这是什么?
      取值 value = o.val(), 设置值 o.val(3)
      
   一个函数怎么可以这样过分, 怎么能根据传入参数的类型和个数不同而行为不同呢? 看不顺眼是不是? 可这就是俺们的价值观. 既然不能防止, 那就故意允许. 虽然形式多变, 却无一句废话. 缺少约束, 不妨碍表达(我不是出来吓人的).
   
5. 链式操作: 线性化的逐步细化
  jQuery早期最主要的卖点就是所谓的链式操作(chain).
 
  $('#content') // 找到content元素
    .find('h3') // 选择所有后代h3节点
    .eq(2)      // 过滤集合, 保留第三个元素
        .html('改变第三个h3的文本')
    .end()      // 返回上一级的h3集合
    .eq(0)
        .html('改变第一个h3的文本');

在一般的命令式语言中, 我们总需要在重重嵌套循环中过滤数据, 实际操作数据的代码与定位数据的代码纠缠在一起. 而jQuery采用先构造集合然后再应用函数于集合的方式实现两种逻辑的解耦, 实现嵌套结构的线性化. 实际上, 我们并不需要借助过程化的思想就可以很直观的理解一个集合, 例如 $('div.my input:checked')可以看作是一种直接的描述,而不是对过程行为的跟踪.
   循环意味着我们的思维处于一种反复回绕的状态, 而线性化之后则沿着一个方向直线前进, 极大减轻了思维负担, 提高了代码的可组合性. 为了减少调用链的中断, jQuery发明了一个绝妙的主意: jQuery包装对象本身类似数组(集合). 集合可以映射到新的集合, 集合可以限制到自己的子集合,调用的发起者是集合,返回结果也是集合,集合可以发生结构上的某种变化但它还是集合, 集合是某种概念上的不动点,这是从函数式语言中吸取的设计思想。集合操作是太常见的操作, 在java中我们很容易发现大量所谓的封装函数其实就是在封装一些集合遍历操作, 而在jQuery中集合操作因为太直白而不需要封装.
   链式调用意味着我们始终拥有一个“当前”对象,所有的操作都是针对这一当前对象进行。这对应于如下公式
     x += dx
调用链的每一步都是对当前对象的增量描述,是针对最终目标的逐步细化过程。Witrix平台中对这一思想也有着广泛的应用。特别是为了实现平台机制与业务代码的融合,平台会提供对象(容器)的缺省内容,而业务代码可以在此基础上进行逐步细化的修正,包括取消缺省的设置等。
  话说回来, 虽然表面上jQuery的链式调用很简单, 内部实现的时候却必须自己多写一层循环, 因为编译器并不知道"自动应用于集合中每个元素"这回事.
  $.fn['someFunc'] = function(){
    return this.each(function(){
      jQuery.someFunc(this,...);
    }
  }
 
6. data: 统一数据管理
  作为一个js库,它必须解决的一个大问题就是js对象与DOM节点之间的状态关联与协同管理问题。有些js库选择以js对象为主,在js对象的成员变量中保存DOM节点指针,访问时总是以js对象为入口点,通过js函数间接操作DOM对象。在这种封装下,DOM节点其实只是作为界面展现的一种底层“汇编”而已。jQuery的选择与Witrix平台类似,都是以HTML自身结构为基础,通过js增强(enhance)DOM节点的功能,将它提升为一个具有复杂行为的扩展对象。这里的思想是非侵入式设计(non-intrusive)和优雅退化机制(graceful degradation)。语义结构在基础的HTML层面是完整的,js的作用是增强了交互行为,控制了展现形式。
  如果每次我们都通过$('#my')的方式来访问相应的包装对象,那么一些需要长期保持的状态变量保存在什么地方呢?jQuery提供了一个统一的全局数据管理机制。
  获取数据 $('#my').data('myAttr')   设置数据 $('#my').data('myAttr',3);
这一机制自然融合了对HTML5的data属性的处理
   <input id="my" data-my-attr="4" ... />
 通过 $('#my').data('myAttr')将可以读取到HTML中设置的数据。
 
 第一次访问data时,jQuery将为DOM节点分配一个唯一的uuid, 然后设置在DOM节点的一个特定的expando属性上, jQuery保证这个uuid在本页面中不重复。
   elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
 以上代码可以同时处理DOM节点和纯js对象的情况。如果是js对象,则data直接放置在js对象自身中,而如果是DOM节点,则通过cache统一管理。
 因为所有的数据都是通过data机制统一管理的,特别是包括所有事件监听函数(data.events),因此jQuery可以安全的实现资源管理。在clone节点的时候,可以自动clone其相关的事件监听函数。而当DOM节点的内容被替换或者DOM节点被销毁的时候,jQuery也可以自动解除事件监听函数, 并安全的释放相关的js数据。
 
7. event:统一事件模型
  "事件沿着对象树传播"这一图景是面向对象界面编程模型的精髓所在。对象的复合构成对界面结构的一个稳定的描述,事件不断在对象树的某个节点发生,并通过冒泡机制向上传播。对象树很自然的成为一个控制结构,我们可以在父节点上监听所有子节点上的事件,而不用明确与每一个子节点建立关联。
  jQuery除了为不同浏览器的事件模型建立了统一抽象之外,主要做了如下增强:
  A. 增加了自定制事件(custom)机制. 事件的传播机制与事件内容本身原则上是无关的, 因此自定制事件完全可以和浏览器内置事件通过同一条处理路径, 采用同样的监听方式. 使用自定制事件可以增强代码的内聚性, 减少代码耦合. 例如如果没有自定制事件, 关联代码往往需要直接操作相关的对象
  $('.switch, .clapper').click(function() {
    var $light = $(this).parent().find('.lightbulb');
    if ($light.hasClass('on')) {
        $light.removeClass('on').addClass('off');
    } else {
        $light.removeClass('off').addClass('on');
    }
  });
而如果使用自定制事件,则表达的语义更加内敛明确,
  $('.switch, .clapper').click(function() {
    $(this).parent().find('.lightbulb').trigger('changeState');
  });
  B. 增加了对动态创建节点的事件监听. bind函数只能将监听函数注册到已经存在的DOM节点上. 例如
    $('li.trigger').bind('click',function(){}}
  如果调用bind之后,新建了另一个li节点,则该节点的click事件不会被监听.
  jQuery的delegate机制可以将监听函数注册到父节点上, 子节点上触发的事件会根据selector被自动派发到相应的handlerFn上. 这样一来现在注册就可以监听未来创建的节点.
    $('#myList').delegate('li.trigger', 'click', handlerFn);
  最近jQuery1.7中统一了bind, live和delegate机制, 天下一统, 只有on/off.
    $('li.trigger’).on('click', handlerFn);  // 相当于bind
    $('#myList’).on('click', 'li.trigger', handlerFn);  // 相当于delegate
    
8. 动画队列:全局时钟协调
  抛开jQuery的实现不谈, 先考虑一下如果我们要实现界面上的动画效果, 到底需要做些什么? 比如我们希望将一个div的宽度在1秒钟之内从100px增加到200px. 很容易想见, 在一段时间内我们需要不时的去调整一下div的宽度, [同时]我们还需要执行其他代码. 与一般的函数调用不同的是, 发出动画指令之后, 我们不能期待立刻得到想要的结果, 而且我们不能原地等待结果的到来. 动画的复杂性就在于:一次性表达之后要在一段时间内执行,而且有多条逻辑上的执行路径要同时展开, 如何协调?
  伟大的艾萨克.牛顿爵士在《自然哲学的数学原理》中写道:"绝对的、真正的和数学的时间自身在流逝着". 所有的事件可以在时间轴上对齐, 这就是它们内在的协调性. 因此为了从步骤A1执行到A5, 同时将步骤B1执行到B5, 我们只需要在t1时刻执行[A1, B1], 在t2时刻执行[A2,B2], 依此类推.
    t1 | t2 | t3 | t4 | t5 ...
    A1 | A2 | A3 | A4 | A5 ...
    B1 | B2 | B3 | B4 | B5 ...
  具体的一种实现形式可以是
  A. 对每个动画, 将其分装为一个Animation对象, 内部分成多个步骤.
      animation = new Animation(div,"width",100,200,1000,
                  负责步骤切分的插值函数,动画执行完毕时的回调函数);
  B. 在全局管理器中注册动画对象
      timerFuncs.add(animation);
  C. 在全局时钟的每一个触发时刻, 将每个注册的执行序列推进一步, 如果已经结束, 则从全局管理器中删除.
     for each animation in timerFuncs
        if(!animation.doOneStep())
           timerFuncs.remove(animation)

  解决了原理问题,再来看看表达问题, 怎样设计接口函数才能够以最紧凑形式表达我们的意图? 我们经常需要面临的实际问题:
  A. 有多个元素要执行类似的动画
  B. 每个元素有多个属性要同时变化
  C. 执行完一个动画之后开始另一个动画
jQuery对这些问题的解答可以说是榨尽了js语法表达力的最后一点剩余价值.
   $('input')
     .animate({left:'+=200px',top:'300'},2000)
     .animate({left:'-=200px',top:20},1000)
     .queue(function(){
       // 这里dequeue将首先执行队列中的后一个函数,因此alert("y")
       $(this).dequeue();
       alert('x');
      })
     .queue(function(){
        alert("y");
        // 如果不主动dequeue, 队列执行就中断了,不会自动继续下去.
        $(this).dequeue();
      });

  A. 利用jQuery内置的selector机制自然表达对一个集合的处理.
  B. 使用Map表达多个属性变化
  C. 利用微格式表达领域特定的差量概念. '+=200px'表示在现有值的基础上增加200px
  D. 利用函数调用的顺序自动定义animation执行的顺序: 在后面追加到执行队列中的动画自然要等前面的动画完全执行完毕之后再启动.
   
  jQuery动画队列的实现细节大概如下所示,
   A. animate函数实际是调用queue(function(){执行结束时需要调用dequeue,否则不会驱动下一个方法})
      queue函数执行时, 如果是fx队列, 并且当前没有正在运行动画(如果连续调用两次animate,第二次的执行函数将在队列中等待),则会自动触发dequeue操作, 驱动队列运行.
      如果是fx队列, dequeue的时候会自动在队列顶端加入"inprogress"字符串,表示将要执行的是动画.
   B. 针对每一个属性,创建一个jQuery.fx对象。然后调用fx.custom函数(相当于start)来启动动画。
   C. custom函数中将fx.step函数注册到全局的timerFuncs中,然后试图启动一个全局的timer.
       timerId = setInterval( fx.tick, fx.interval );
   D. 静态的tick函数中将依次调用各个fx的step函数。step函数中通过easing计算属性的当前值,然后调用fx的update来更新属性。
   E. fx的step函数中判断如果所有属性变化都已完成,则调用dequeue来驱动下一个方法。

  很有意思的是, jQuery的实现代码中明显有很多是接力触发代码: 如果需要执行下一个动画就取出执行, 如果需要启动timer就启动timer等. 这是因为js程序是单线程的,真正的执行路径只有一条,为了保证执行线索不中断, 函数们不得不互相帮助一下. 可以想见, 如果程序内部具有多个执行引擎, 甚至无限多的执行引擎, 那么程序的面貌就会发生本质性的改变. 而在这种情形下, 递归相对于循环而言会成为更自然的描述.
 
9. promise模式:因果关系的识别
  现实中,总有那么多时间线在独立的演化着, 人与物在时空中交错,却没有发生因果. 软件中, 函数们在源代码中排着队, 难免会产生一些疑问, 凭什么排在前面的要先执行? 难道没有它就没有我? 让全宇宙喊着1,2,3齐步前进, 从上帝的角度看,大概是管理难度过大了, 于是便有了相对论. 如果相互之间没有交换信息, 没有产生相互依赖, 那么在某个坐标系中顺序发生的事件, 在另外一个坐标系中看来, 就可能是颠倒顺序的. 程序员依葫芦画瓢, 便发明了promise模式.
  promise与future模式基本上是一回事,我们先来看一下java中熟悉的future模式.
  futureResult = doSomething();
  ...
  realResult = futureResult.get();
  发出函数调用仅仅意味着一件事情发生过, 并不必然意味着调用者需要了解事情最终的结果. 函数立刻返回的只是一个将在未来兑现的承诺(Future类型), 实际上也就是某种句柄. 句柄被传来传去, 中间转手的代码对实际结果是什么,是否已经返回漠不关心. 直到一段代码需要依赖调用返回的结果, 因此它打开future, 查看了一下. 如果实际结果已经返回, 则future.get()立刻返回实际结果, 否则将会阻塞当前的执行路径, 直到结果返回为止. 此后再调用future.get()总是立刻返回, 因为因果关系已经被建立, [结果返回]这一事件必然在此之前发生, 不会再发生变化.
  future模式一般是外部对象主动查看future的返回值, 而promise模式则是由外部对象在promise上注册回调函数.
  function getData(){
   return $.get('/foo/').done(function(){
      console.log('Fires after the AJAX request succeeds');
   }).fail(function(){
      console.log('Fires after the AJAX request fails');
   });
  }
 
  function showDiv(){
    var dfd = $.Deferred();
    $('#foo').fadeIn( 1000, dfd.resolve );
    return dfd.promise();
  }
 
  $.when( getData(), showDiv() )
    .then(function( ajaxResult, ignoreResultFromShowDiv ){
        console.log('Fires after BOTH showDiv() AND the AJAX request succeed!');
        // 'ajaxResult' is the server’s response
    });
  jQuery引入Deferred结构, 根据promise模式对ajax, queue, document.ready等进行了重构, 统一了异步执行机制. then(onDone, onFail)将向promise中追加回调函数, 如果调用成功完成(resolve), 则回调函数onDone将被执行, 而如果调用失败(reject), 则onFail将被执行. when可以等待在多个promise对象上. promise巧妙的地方是异步执行已经开始之后甚至已经结束之后,仍然可以注册回调函数
  someObj.done(callback).sendRequest() vs. someObj.sendRequest().done(callback)
 callback函数在发出异步调用之前注册或者在发出异步调用之后注册是完全等价的, 这揭示出程序表达永远不是完全精确的, 总存在着内在的变化维度. 如果能有效利用这一内在的可变性, 则可以极大提升并发程序的性能.
   promise模式的具体实现很简单. jQuery._Deferred定义了一个函数队列,它的作用有以下几点:
   A. 保存回调函数。
   B. 在resolve或者reject的时刻把保存着的函数全部执行掉。
   C. 已经执行之后, 再增加的函数会被立刻执行。
 
   一些专门面向分布式计算或者并行计算的语言会在语言级别内置promise模式, 比如E语言.
     def carPromise := carMaker <- produce("Mercedes");
     def temperaturePromise := carPromise <- getEngineTemperature()
     ...
     when (temperaturePromise) -> done(temperature) {
       println(`The temperature of the car engine is: $temperature`)
     } catch e {
       println(`Could not get engine temperature, error: $e`)
     }
  在E语言中, <-是eventually运算符, 表示最终会执行, 但不一定是现在. 而普通的car.moveTo(2,3)表示立刻执行得到结果. 编译器负责识别所有的promise依赖, 并自动实现调度.
 
10. extend: 继承不是必须的
  js是基于原型的语言, 并没有内置的继承机制, 这一直让很多深受传统面向对象教育的同学们耿耿于怀. 但继承一定是必须的吗? 它到底能够给我们带来什么? 最纯朴的回答是: 代码重用. 那么, 我们首先来分析一下继承作为代码重用手段的潜力.
  曾经有个概念叫做"多重继承", 它是继承概念的超级赛亚人版, 很遗憾后来被诊断为存在着先天缺陷, 以致于出现了一种对于继承概念的解读: 继承就是"is a"关系, 一个派生对象"is a"很多基类, 必然会出现精神分裂, 所以多重继承是不好的.
   class A{ public: void f(){ f in A } }
   class B{ public: void f(){ f in B } }
   class D: public A, B{}
 如果D类从A,B两个基类继承, 而A和B类中都实现了同一个函数f, 那么D类中的f到底是A中的f还是B中的f, 抑或是A中的f+B中的f呢? 这一困境的出现实际上源于D的基类A和B是并列关系, 它们满足交换律和结合律, 毕竟,在概念层面上我们可能难以认可两个任意概念之间会出现从属关系. 但如果我们放松一些概念层面的要求, 更多的从操作层面考虑一下代码重用问题, 可以简单的认为B在A的基础上进行操作, 那么就可以得到一个线性化的结果. 也就是说, 放弃A和B之间的交换律只保留结合律, extends A, B 与 extends B,A 会是两个不同的结果, 不再存在诠释上的二义性. scala语言中的所谓trait(特性)机制实际上采用的就是这一策略.
  面向对象技术发明很久之后, 出现了所谓的面向方面编程(AOP), 它与OOP不同, 是代码结构空间中的定位与修改技术. AOP的眼中只有类与方法, 不知道什么叫做意义. AOP也提供了一种类似多重继承的代码重用手段, 那就是mixin. 对象被看作是可以被打开,然后任意修改的Map, 一组成员变量与方法就被直接注射到对象体内, 直接改变了它的行为.
  prototype.js库引入了extend函数,
  Object.extend = function(destination, source) {
    for (var property in source) {
      destination[property] = source[property];
    }
    return destination;
  }
  就是Map之间的一个覆盖运算, 但很管用, 在jQuery库中也得到了延用. 这个操作类似于mixin, 在jQuery中是代码重用的主要技术手段---没有继承也没什么大不了的.

11. 名称映射: 一切都是数据
  代码好不好, 循环判断必须少. 循环和判断语句是程序的基本组成部分, 但是优良的代码库中却往往找不到它们的踪影, 因为这些语句的交织会模糊系统的逻辑主线, 使我们的思想迷失在疲于奔命的代码追踪中. jQuery本身通过each, extend等函数已经极大减少了对循环语句的需求, 对于判断语句, 则主要是通过映射表来处理. 例如, jQuery的val()函数需要针对不同标签进行不同的处理, 因此定义一个以tagName为key的函数映射表
   valHooks: { option: {get:function(){}}}
这样在程序中就不需要到处写
   if(elm.tagName == 'OPTION'){
     return ...;
   }else if(elm.tagName == 'TEXTAREA'){
     return ...;
   }
可以统一处理
   (valHooks[elm.tagName.toLowerCase()] || defaultHandler).get(elm);
   
  映射表将函数作为普通数据来管理, 在动态语言中有着广泛的应用. 特别是, 对象本身就是函数和变量的容器, 可以被看作是映射表. jQuery中大量使用的一个技巧就是利用名称映射来动态生成代码, 形成一种类似模板的机制. 例如为了实现myWidth和myHeight两个非常类似的函数, 我们不需要
  jQuery.fn.myWidth = function(){
      return parseInt(this.style.width,10) + 10;
    }
    
    jQuery.fn.myHeight = function(){
      return parseInt(this.style.height,10) + 10;
    }
而可以选择动态生成
    jQuery.each(['Width','Height'],function(name){
      jQuery.fn['my'+name] = function(){
        return parseInt(this.style[name.toLowerCase()],10) + 10;
      }
    });
 
12. 插件机制:其实我很简单    
  jQuery所谓的插件其实就是$.fn上增加的函数, 那这个fn是什么东西?
  (function(window,undefined){
    // 内部又有一个包装
    var jQuery = (function() {
      var jQuery = function( selector, context ) {
            return new jQuery.fn.init( selector, context, rootjQuery );
        }
       ....
      // fn实际就是prototype的简写
      jQuery.fn = jQuery.prototype = {
          constructor: jQuery,
          init: function( selector, context, rootjQuery ) {...  }
      }
    
      // 调用jQuery()就是相当于new init(), 而init的prototype就是jQuery的prototype
      jQuery.fn.init.prototype = jQuery.fn;
    
      // 这里返回的jQuery对象只具备最基本的功能, 下面就是一系列的extend
      return jQuery;
    })();  
    ...
     // 将jQuery暴露为全局对象
    window.jQuery = window.$ = jQuery;
  })(window);
  显然, $.fn其实就是jQuery.prototype的简写.
 
  无状态的插件仅仅就是一个函数, 非常简单.
  // 定义插件
  (function($){
      $.fn.hoverClass = function(c) {
          return this.hover(
              function() { $(this).toggleClass(c); }
          );
      };
  })(jQuery);
 
  // 使用插件
  $('li').hoverClass('hover');
 
 对于比较复杂的插件开发, jQuery UI提供了一个widget工厂机制,
 $.widget("ui.dialog", {
   options: {
        autoOpen: true,...
     },
     _create: function(){ ... },
     _init: function() {
        if ( this.options.autoOpen ) {
            this.open();
        }
     },
     _setOption: function(key, value){ ... }
     destroy: function(){ ... }
 });
 
 调用 $('#dlg').dialog(options)时, 实际执行的代码基本如下所示:
  this.each(function() {
        var instance = $.data( this, "dialog" );
        if ( instance ) {
            instance.option( options || {} )._init();
        } else {
            $.data( this, "dialog", new $.ui.dialog( options, this ) );
        }
    }
 可以看出, 第一次调用$('#dlg').dialog()函数时会创建窗口对象实例,并保存在data中, 此时会调用_create()和_init()函数, 而如果不是第一次调用, 则是在已经存在的对象实例上调用_init()方法. 多次调用$('#dlg').dialog()并不会创建多个实例.

13. browser sniffer vs. feature detection
  浏览器嗅探(browser sniffer)曾经是很流行的技术, 比如早期的jQuery中
  jQuery.browser = {
        version:(userAgent.match(/.+(?:rv|it|ra|ie)[/: ]([d.]+)/) || [0,'0'])[1],
        safari:/webkit/.test(userAgent),
        opera:/opera/.test(userAgent),
        msie:/msie/.test(userAgent) && !/opera/.test(userAgent),
        mozilla:/mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent)
  };
  在具体代码中可以针对不同的浏览器作出不同的处理
  if($.browser.msie) {
      // do something
  } else if($.browser.opera) {
      // ...
  }  

  但是随着浏览器市场的竞争升级, 竞争对手之间的互相模仿和伪装导致userAgent一片混乱, 加上Chrome的诞生, Safari的崛起, IE也开始加速向标准靠拢, sniffer已经起不到积极的作用. 特性检测(feature detection)作为更细粒度, 更具体的检测手段, 逐渐成为处理浏览器兼容性的主流方式.
  jQuery.support = {
        // IE strips leading whitespace when .innerHTML is used
        leadingWhitespace: ( div.firstChild.nodeType === 3 ),
        ...
    }
    只基于实际看见的,而不是曾经知道的, 这样更容易做到兼容未来.

14. Prototype vs. jQuery
  prototype.js是一个立意高远的库, 它的目标是提供一种新的使用体验,参照Ruby从语言级别对javascript进行改造,并最终真的极大改变了js的面貌。$, extends, each, bind...这些耳熟能详的概念都是prototype.js引入到js领域的. 它肆无忌惮的在window全局名字空间中增加各种概念, 大有谁先占坑谁有理, 舍我其谁的气势. 而jQuery则扣扣索索, 抱着比较实用化的理念, 目标仅仅是write less, do more而已.  
  不过等待激进的理想主义者的命运往往都是壮志未酬身先死. 当prototype.js标志性的bind函数等被吸收到ECMAScript标准中时, 便注定了它的没落. 到处修改原生对象的prototype, 这是prototype.js的独门秘技, 也是它的死穴. 特别是当它试图模仿jQuery, 通过Element.extend(element)返回增强对象的时候, 算是彻底被jQuery给带到沟里去了. prototype.js与jQuery不同, 它总是直接修改原生对象的prototype, 而浏览器却是充满bug, 谎言, 历史包袱并夹杂着商业阴谋的领域, 在原生对象层面解决问题注定是一场悲剧. 性能问题, 名字冲突, 兼容性问题等等都是一个帮助库的能力所无法解决的. Prototype.js的2.0版本据说要做大的变革, 不知是要与历史决裂, 放弃兼容性, 还是继续挣扎, 在夹缝中求生.

posted @ 2011-12-25 21:23 canonical 阅读(2323) | 评论 (5)编辑 收藏

1. C语言抽象出了软件所在的领域(domain): 由变量v1,v2,...和函数f1,f2,...组成的空间

2. 面向对象(OOP)指出,在这一领域上可以建立分组(group)结构:一组相关的变量和函数构成一个集合,我们称之为对象(Object)。同时在分组结构上可以定义一个运算(推理)关系:  D > B, 派生类D从基类B继承(inheritance),相应的派生对象符合基类对象所满足的所有约束。推理是有价值的,因为根据 D > B, B > A 可以自动推导出 D > A,所有针对A的断言在理论上对D都成立(这也就是我们常说的“派生对象 is a 基类对象”)。编译器也能有点智能了。
   一个有趣的地方是,D > B意味着在D和B之间存在着某种差异,但是我们却无法把它显式的表达出来!也就是说在代码层面上我们无法明确表达 D - B是什么。为了把更多的信息不断的导入到原有系统中,面向对象内置提供的方法是建立不断扩展的类型树,类型树每增长一层,就可以多容纳一些新的信息。这是一种金字塔式的结构,只不过是一种倒立的金字塔,最终基点会被不断增长的结构压力所压垮。

3. 组件技术(Component)本质上是在提倡面向接口(interface),然后通过接口之间的组合(Composition)而不是对象之间的继承(inheritance)来构造系统。基于组合的观念相当于是定义了运算关系:D = B + C。终于,我们勉强可以在概念层面上做加法了。
   组件允许我们随意的组合,按照由简单到复杂的方向构造系统,但是组件构成的成品之间仍然无法自由的建立关系。这意味着组件组装得到的成品只是某种孤立的,偶然的产物。
   F = A + B + C  ? G = A + D + C。

4. 在数学上,配备了加法运算的集合构成半群,如果要成为群(Group),则必须定义相应的逆运算:减法。 群结构使得大粒度的结构变换成为可能。
   F = A + B + C = A + D - D + B + C = (A + D + C) - D + B = G - D + B
   在不破坏原有代码的情况下,对原有系统功能进行增删,这就是面向切面(AOP)技术的全部价值。


posted @ 2011-05-08 12:07 canonical 阅读(3632) | 评论 (1)编辑 收藏