|
2006年5月19日
谨以此文庆祝清华建校108周年,及5字班本科毕业20周年 作者: Canonical 众所周知,计算机科学得以存在的基石是两个基本理论:图灵于1936年提出的图灵机理论和丘奇同年早期发表的Lambda演算理论。这两个理论奠定了所谓通用计算(Universal Computation)的概念基础,描绘了具有相同计算能力(图灵完备),但形式上却南辕北辙、大相径庭的两条技术路线。如果把这两种理论看作是上帝所展示的世界本源面貌的两个极端,那么是否存在一条更加中庸灵活的到达通用计算彼岸的中间路径? 自1936年以来,软件作为计算机科学的核心应用,一直处在不间断的概念变革过程中,各类程序语言/系统架构/设计模式/方法论层出不穷,但是究其软件构造的基本原理,仍然没有脱出两个基本理论最初所设定的范围。如果定义一种新的软件构造理论,它所引入的新概念本质上能有什么特异之处?能够解决什么棘手的问题? 本文中笔者提出在图灵机和lambda演算的基础上可以很自然的引入一个新的核心概念--可逆性,从而形成一个新的软件构造理论--可逆计算(Reversible Computation)。可逆计算提供了区别于目前业内主流方法的更高层次的抽象手段,可以大幅降低软件内在的复杂性,为粗粒度软件复用扫除了理论障碍。 可逆计算的思想来源不是计算机科学本身,而是理论物理学,它将软件看作是处于不断演化过程中的抽象实体, 在不同的复杂性层次上由不同的运算规则所描述,它所关注的是演化过程中产生的微小差量如何在系统内有序的传播并发生相互作用。 本文第一节将介绍可逆计算理论的基本原理与核心公式,第二节分析可逆计算理论与组件和模型驱动等传统软件构造理论的区别和联系,并介绍可逆计算理论在软件复用领域的应用,第三节从可逆计算角度解构Docker、React等创新技术实践。 一. 可逆计算的基本原理可逆计算可以看作是在真实的信息有限的世界中,应用图灵计算和lambda演算对世界建模的一种必然结果,我们可以通过以下简单的物理图像来理解这一点。 首先,图灵机是一种结构固化的机器,它具有可枚举的有限的状态集合,只能执行有限的几条操作指令,但是可以从无限长的纸带上读取和保存数据。例如我们日常使用的电脑,它在出厂的时候硬件功能就已经确定了,但是通过安装不同的软件,传入不同的数据文件,最终它可以自动产生任意复杂的目标输出。图灵机的计算过程在形式上可以写成 ![目标输出 = 固定机器(无限复杂的输入)\\](https://www.zhihu.com/equation?tex=%E7%9B%AE%E6%A0%87%E8%BE%93%E5%87%BA+%3D+%E5%9B%BA%E5%AE%9A%E6%9C%BA%E5%99%A8%EF%BC%88%E6%97%A0%E9%99%90%E5%A4%8D%E6%9D%82%E7%9A%84%E8%BE%93%E5%85%A5%EF%BC%89%5C%5C)
与图灵机相反的是,lambda演算的核心概念是函数,一个函数就是一台小型的计算机器,函数的复合仍然是函数,也就是说可以通过机器和机器的递归组合来产生更加复杂的机器。lambda演算的计算能力与图灵机等价,这意味着如果允许我们不断创建更加复杂的机器,即使输入一个常数0,我们也可以得到任意复杂的目标输出。lambda演算的计算过程在形式上可以写成
可以看出,以上两种计算过程都可以被表达为Y=F(X) 这样一种抽象的形式。如果我们把Y=F(X)理解为一种建模过程,即我们试图理解输入的结构以及输入和输出之间的映射关系,采用最经济的方式重建输出,则我们会发现图灵机和lambda演算都假定了现实世界中无法满足的条件。在真实的物理世界中,人类的认知总是有限的,所有的量都需要区分已知的部分和未知的部分,因此我们需要进行如下分解:
重新整理一下符号,我们就得到了一个适应范围更加广泛的计算模式
除了函数运算F(X)之外,这里出现了一个新的结构运算符⊕,它表示两个元素之间的合成运算,并不是普通数值意义上的加法,同时引出了一个新的概念:差量△。△的特异之处在于,它必然包含某种负元素,F(X)与△合并在一起之后的结果并不一定是“增加”了输出,而完全可能是“减少”。 在物理学中,差量△存在的必然性以及△包含逆元这一事实完全是不言而喻的,因为物理学的建模必须要考虑到两个基本事实: - 世界是“测不准”的,噪声永远存在
- 模型的复杂度要和问题内在的复杂度相匹配,它捕获的是问题内核中稳定不变的趋势及规律。
例如,对以下的数据 ![](https://pic4.zhimg.com/80/v2-91f19a10faa36653267ffbd4eab86b7f_hd.jpg) 我们所建立的模型只能是类似图(a)中的简单曲线,图(b)中的模型试图精确拟合每一个数据点在数学上称之为过拟合,它难以描述新的数据,而图(c)中限制差量只能为正值则会极大的限制模型的描述精度。 以上是对Y=F(X)⊕△这一抽象计算模式的一个启发式说明,下面我们将介绍在软件构造领域落实这一计算模式的一种具体技术实现方案,笔者将其命名为可逆计算。 所谓可逆计算,是指系统化的应用如下公式指导软件构造的一种技术路线
App : 所需要构建的目标应用程序 DSL: 领域特定语言(Domain Specific Language),针对特定业务领域定制的业务逻辑描述语言,也是所谓领域模型的文本表示形式 Generator : 根据领域模型提供的信息,反复应用生成规则可以推导产生大量的衍生代码。实现方式包括独立的代码生成工具,以及基于元编程(Metaprogramming)的编译期模板展开 Biz : 根据已知模型推导生成的逻辑与目标应用程序逻辑之间的差异被识别出来,并收集在一起,构成独立的差量描述 aop_extends: 差量描述与模型生成部分通过类似面向切面编程(Aspect Oriented Programming)的技术结合在一起,这其中涉及到对模型生成部分的增加、修改、替换、删除等一系列操作 DSL是对关键性领域信息的一种高密度的表达,它直接指导Generator生成代码,这一点类似于图灵计算通过输入数据驱动机器执行内置指令。而如果把Generator看作是文本符号的替换生成,则它的执行和复合规则完全就是lambda演算的翻版。差量合并在某种意义上是一种很新奇的操作,因为它要求我们具有一种细致入微、无所不达的变化收集能力,能够把散布系统各处的同阶小量分离出来并合并在一起,这样差量才具有独立存在的意义和价值。同时,系统中必须明确建立逆元和逆运算的概念,在这样的概念体系下差量作为“存在”与“不存在”的混合体才可能得到表达。 现有的软件基础架构如果不经过彻底的改造,是无法有效的实施可逆计算的。正如图灵机模型孕育了C语言,Lambda演算促生了Lisp语言一样,为了有效支持可逆计算,笔者提出了一种新的程序语言X语言,它内置了差量定义、生成、合并、拆分等关键特性,可以快速建立领域模型,并在领域模型的基础上实现可逆计算。 为了实施可逆计算,我们必须要建立差量的概念。变化产生差量,差量有正有负,而且应该满足下面三条要求: - 差量独立存在
- 差量相互作用
- 差量具有结构
在第三节中笔者将会以Docker为实例说明这三条要求的重要性。 可逆计算的核心是“可逆”,这一概念与物理学中熵的概念息息相关,它的重要性其实远远超出了程序构造本身,在可逆计算的方法论来源一文中,笔者会对它有更详细的阐述。 正如复数的出现扩充了代数方程的求解空间,可逆计算为现有的软件构造技术体系补充了“可逆的差量合并”这一关键性技术手段,从而极大扩充了软件复用的可行范围,使得系统级的粗粒度软件复用成为可能。同时在新的视角下,很多原先难以解决的模型抽象问题可以找到更加简单的解决方案,从而大幅降低了软件构造的内在复杂性。在第二节中笔者将会对此进行详细阐述。 软件开发虽然号称是知识密集性的工作,但到目前为止,众多一线程序员的日常中仍然包含着大量代码拷贝/粘贴/修改的机械化手工操作内容,而在可逆计算理论中,代码结构的修改被抽象为可自动执行的差量合并规则,因此通过可逆计算,我们可以为软件自身的自动化生产创造基础条件。笔者在可逆计算理论的基础上,提出了一个新的软件工业化生产模式NOP(Nop is nOt Programming),以非编程的方式批量生产软件。NOP不是编程,但也不是不编程,它强调的是将业务人员可以直观理解的逻辑与纯技术实现层面的逻辑相分离,分别使用合适的语言和工具去设计,然后再把它们无缝的粘接在一起。笔者将在另一篇文章中对NOP进行详细介绍。 可逆计算与可逆计算机有着同样的物理学思想来源,虽然具体的技术内涵并不一致,但它们目标却是统一的。正如云计算试图实现计算的云化一样,可逆计算和可逆计算机试图实现的都是计算的可逆化。 二. 可逆计算对传统理论的继承和发展软件的诞生源于数学家研究希尔伯特第十问题时的副产品,早期软件的主要用途也是数学物理计算,那时软件中的概念无疑是抽象的、数学化的。随着软件的普及,越来越多应用软件的研发催生了面向对象和组件化的方法论,它试图弱化抽象思维,转而贴近人类的常识,从人们的日常经验中汲取知识,把业务领域中人们可以直观感知的概念映射为软件中的对象,仿照物质世界的生产制造过程从无到有、从小到大,逐步拼接组装实现最终软件产品的构造。 像框架、组件、设计模式、架构视图等软件开发领域中耳熟能详的概念,均直接来自于建筑业的生产经验。组件理论继承了面向对象思想的精华,借助可复用的预制构件这一概念,创造了庞大的第三方组件市场,获得了空前的技术和商业成功,即使到今天仍然是最主流的软件开发指导思想。但是,组件理论内部存在着一个本质性的缺陷,阻碍了它把自己的成功继续推进到一个新的高度。 我们知道,所谓复用就是对已有的制成品的重复使用。为了实现组件复用,我们需要找到两个软件中的公共部分,把它分离出来并按照组件规范整理成标准形式。但是,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这样的情况,即使多出来的部分一模一样,我们也无法实现这部分内容的重用。组件技术明确指出"组合优于继承",这相当于引入了加法
这样就可以抽象出组件C进行重用。 沿着上述方向推演下去,我们很容易确定下一步的发展是引入“减法”,这样才可以把 A = B + C看作是一个真正的方程,通过方程左右移项求解出
通过减法引入的“负组件”是一个全新的概念,它为软件复用打开了一扇新的大门。 假设我们已经构建好了系统 X = D + E + F, 现在需要构建 Y = D + E + G。如果遵循组件的解决方案,则需要将X拆解为多个组件,然后更换组件F为G后重新组装。而如果遵循可逆计算的技术路线,通过引入逆元 -F, 我们立刻得到
在不拆解X的情况下,通过直接追加一个差量△Y,即可将系统X转化为系统Y。 组件的复用条件是“相同方可复用”,但在存在逆元的情况下,具有最大颗粒度的完整系统X在完全不改的情况下直接就可以被复用,软件复用的范围被拓展为“相关即可复用”,软件复用的粒度不再有任何限制。组件之间的关系也发生了深刻的变化,不再是单调的构成关系,而成为更加丰富多变的转化关系。 Y = X + △Y 这一物理图像对于复杂软件产品的研发具有非常现实的意义。X可以是我们所研发的软件产品的基础版本或者说主版本,在不同的客户处部署实施时,大量的定制化需求被隔离到独立的差量△Y中,这些定制的差量描述单独存放,通过编译技术与主版本代码再合并到一起。主版本的架构设计和代码实现只需要考虑业务领域内稳定的核心需求,不会受到特定客户处偶然性需求的冲击,从而有效的避免架构腐化。主版本研发和多个项目的实施可以并行进行,不同的实施版本对应不同的△Y,互不影响,同时主版本的代码与所有定制代码相互独立,能够随时进行整体升级。 模型驱动架构(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的程序构造可以表述为如下公式
MDA的愿景是像C语言取代汇编那样最终彻底消灭传统编程语言。但经历了这么多年发展之后,它仍未能够在广泛的应用领域中展现出相对于传统编程压倒性的竞争优势。 事实上,目前基于MDA的开发工具在面对多变的业务领域时,总是难掩其内在的不适应性。根据本文第一节的分析,我们知道建模必须要考虑差量。而在MDA的构造公式中,左侧的App代表了各种未知需求,而右侧的Transformer和PIM的设计器实际上都主要由开发工具厂商提供,未知=已知这样一个方程是无法持久保持平衡的。 目前,工具厂商的主要做法是提供大而全的模型集合,试图事先预测用户所有可能的业务场景。但是,我们知道“天下没有免费的午餐”,模型的价值在于体现了业务领域中的本质性约束,没有任何一个模型是所有场景下都最优的。预测需求会导致出现一种悖论: 模型内置假定过少,则无法根据用户输入的少量信息自动生成大量有用的工作,也无法防止用户出现误操作,模型的价值不明显,而如果反之,模型假定很多,则它就会固化到某个特定业务场景,难以适应新的情况。 打开一个MDA工具的设计器,我们最经常的感受是大部分选项都不需要,也不知道是干什么用的,需要的选项却到处找也找不到。 可逆计算对MDA的扩展体现为两点: - 可逆计算中Generator和DSL都是鼓励用户扩充和调整的,这一点类似于面向语言编程(Language-oriented programming)。
- 存在一个额外的差量定制机会,可以对整体生成结果进行精确的局部修正。
在笔者提出的NOP生产模式中,必须要包含一个新的关键组件:设计器的设计器。普通的程序员可以利用设计器的设计器快速设计开发自己的领域特定语言(DSL)及其可视化设计器,同时可以通过设计器的设计器对系统中的任意设计器进行定制调整,自由的增加或者删除元素。 面向切面(AOP)是与面向对象(OOP)互补的一种编程范式,它可以实现对那些横跨多个对象的所谓横切关注点(cross-cutting concern)的封装。例如,需求规格中可能规定所有的业务操作都要记录日志,所有的数据库修改操作都要开启事务。如果按照面向对象的传统实现方式,需求中的一句话将会导致众多对象类中陡然膨胀出现大量的冗余代码,而通过AOP, 这些公共的“修饰性”的操作就可以被剥离到独立的切面描述中。这就是所谓纵向分解和横向分解的正交性。 ![](http://www.blogjava.net/images/blogjava_net/canonical/aop.png)
AOP本质上是两个能力的组合: - 在程序结构空间中定位到目标切点(Pointcut)
- 对局部程序结构进行修改,将扩展逻辑(Advice)编织(Weave)到指定位置。
定位依赖于存在良好定义的整体结构坐标系(没有坐标怎么定位?),而修改依赖于存在良好定义的局部程序语义结构。目前主流的AOP技术的局限性在于,它们都是在面向对象的语境下表达的,而领域结构与对象实现结构并不总是一致的,或者说用对象体系的坐标去表达领域语义是不充分的。例如,申请人和审批人在领域模型中是需要明确区分的不同的概念,但是在对象层面却可能都对应于同样的Person类,使用AOP的很多时候并不能直接将领域描述转换为切点定义和Advice实现。这种限制反映到应用层面,结果就是除了日志、事务、延迟加载、缓存等少数与特定业务领域无关的“经典”应用之外,我们找不到AOP的用武之地。 可逆计算需要类似AOP的定位和结构修正能力,但是它是在领域模型空间中定义这些能力的,因而大大扩充了AOP的应用范围。特别是,可逆计算中领域模型自我演化产生的结构差量△能够以类似AOP切面的形式得到表达。 我们知道,组件可以标识出程序中反复出现的“相同性”,而可逆计算可以捕获程序结构的“相似性”。相同很罕见,需要敏锐的甄别,但是在任何系统中,有一种相似性都是唾手可得的,即动力学演化过程中系统与自身历史快照之间的相似性。这种相似性在此前的技术体系中并没有专门的技术表达形式。 通过纵向和横向分解,我们所建立的概念之网存在于一个设计平面当中,当设计平面沿着时间轴演化时,很自然的会产生一个“三维”映射关系:后一时刻的设计平面可以看作是从前一时刻的平面增加一个差量映射(定制)而得到,而差量是定义在平面的每一个点上的。这一图像类似于范畴论(Category Theory)中的函子(Functor)概念,可逆计算中的差量合并扮演了函子映射的角色。因此,可逆计算相当于扩展了原有的设计空间,为演化这一概念找到了具体的一种技术表现形式。 软件产品线理论源于一个洞察,即在一个业务领域中,很少有软件系统是完全独特的,大量的软件产品之间存在着形式和功能的相似性,可以归结为一个产品家族,把一个产品家族中的所有产品(已存在的和尚未存在的)作为一个整体来研究、开发、演进,通过科学的方法提取它们的共性,结合有效的可变性管理,就有可能实现规模化、系统化的软件复用,进而实现软件产品的工业化生产。 软件产品线工程采用两阶段生命周期模型,区分领域工程和应用工程。所谓领域工程,是指分析业务领域内软件产品的共性,建立领域模型及公共的软件产品线架构,形成可复用的核心资产的过程,即面向复用的开发(development for reuse)。而应用工程,其实质是使用复用来开发( development with reuse),也就是利用已经存在的体系架构、需求、测试、文档等核心资产来制造具体应用产品的生产活动。 卡耐基梅隆大学软件工程研究所(CMU-SEI)的研究人员在2008年的报告中宣称软件产品线可以带来如下好处: - 提升10倍以上生产率
- 提升10倍以上产品质量
- 缩减60%以上成本
- 缩减87%以上人力需求
- 缩减98%以上产品上市时间
- 进入新市场的时间以月计,而不是年
软件产品线描绘的理想非常美好:复用度90%以上的产品级复用、随需而变的敏捷定制、无视技术变迁影响的领域架构、优异可观的经济效益等等。它所存在的唯一问题就是如何才能做到?尽管软件产品线工程试图通过综合利用所有管理的和技术的手段,在组织级别策略性的复用一切技术资产(包括文档、代码、规范、工具等等),但在目前主流的技术体制下,发展成功的软件产品线仍然面临着重重困难。 可逆计算的理念与软件产品线理论高度契合,它的技术方案为软件产品线的核心技术困难---可变性管理带来了新的解决思路。在软件产品线工程中,传统的可变性管理主要是适配、替换和扩展这三种方式: ![](http://www.blogjava.net/images/blogjava_net/canonical/product_line.png)
这三种方式都可以看作是向核心架构补充功能。但是可复用性的障碍不仅仅是来自于无法追加新的功能,很多时候也在于无法屏蔽原先已经存在的功能。传统的适配技术等要求接口一致匹配,是一种刚性的对接要求,一旦失配必将导致不断向上传导应力,最终只能通过整体更换组件来解决问题。可逆计算通过差量合并为可变性管理补充了“消除”这一关键性机制,可以按需在领域模型空间中构建出柔性适配接口,从而有效的控制变化点影响范围。 可逆计算中的差量虽然也可以被解释为对基础模型的一种扩展,但是它与插件扩展技术之间还是存在着明显的区别。在平台-插件这样的结构中,平台是最核心的主体,插件依附于平台而存在,更像是一种补丁机制,在概念层面上是相对次要的部分。而在可逆计算中,通过一些形式上的变换,我们可以得到一个对称性更高的公式:
如果把G看作是一种相对不变的背景知识,则形式上我们可以把它隐藏起来,定义一个更加高级的“括号”运算符,它类似于数学中的“内积”。在这种形式下,B和D是对偶的,B是对D的补充,而D也是对B的补充。同时,我们注意到G(D)是模型驱动架构的体现,模型驱动之所以有价值就在于模型D中发生的微小变化,可以被G放大为系统各处大量衍生的变化,因此G(D)是一种非线性变换,而B是系统中去除D所对应的非线性因素之后剩余的部分。当所有复杂的非线性影响因素都被剥离出去之后,最后剩下的部分B就有可能是简单的,甚至能够形成一种新的可独立理解的领域模型结构(可以类比声波与空气的关系,声波是空气的扰动,但是不用研究空气本体,我们就可以直接用正弦波模型来描述声波)。 A = (B,D)的形式可以直接推广到存在更多领域模型的情况
因为B、D、E等概念都是某种DSL所描述的领域模型,因此它们可以被解释为A投影到特定领域模型子空间所产生的分量,也就是说,应用A可以被表示为一个“特征向量”(Feature Vector), 例如
与软件产品线中常用的面向特征编程(Feature Oriented Programming)相比,可逆计算的特征分解方案强调领域特定描述,特征边界更加明确,特征合成时产生的概念冲突更容易处理。 特征向量本身构成更高维度的领域模型,它可以被进一步分解下去,从而形成一个模型级列,例如定义
, 并且假设D'可以继续分解
,则可以得到
最终我们可以通过领域特征向量U'来描述D’,然后再通过领域特征向量D‘来描述原有的模型A。 可逆计算的这一构造策略类似于深度神经网络,它不再局限于具有极多可调参数的单一模型,而是建立抽象层级不同、复杂性层级不同的一系列模型,通过逐步求精的方式构造出最终的应用。 在可逆计算的视角下,应用工程的工作内容变成了使用特征向量来描述软件需求,而领域工程则负责根据特征向量描述来生成最终的软件。 三. 初露端倪的差量革命(一)DockerDocker是2013年由创业公司dotCloud开源的应用容器引擎,它可以将任何应用及其依赖的环境打包成一个轻量级、可移植、自包含的容器(Container),并据此以容器为标准化单元创造了一种新型的软件开发、部署和交付形式。 Docker一出世就秒杀了Google的亲儿子lmctfy (Let Me Contain That For You)容器技术,同时也把Google的另一个亲儿子Go语言迅速捧成了网红,之后Docker的发展 更是一发而不可收拾。2014年开始一场Docker风暴席卷全球,以前所未有的力度推动了操作系统内核的变革,在众多巨头的跟风造势下瞬间引爆容器云市场,真正从根本上改变了企业应用从开发、构建到部署、运行整个生命周期的技术形态。 ![](https://pic2.zhimg.com/80/v2-d6ef1c89995987f99c69e2c9f2456985_hd.jpg) Docker技术的成功源于它对软件运行时复杂性的本质性降低,而它的技术方案可以看作是可逆计算理论的一种特例。Docker的核心技术模式可以用如下公式进行概括
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的独特之处。 - 差量独立存在:Docker最重要的价值就在于通过容器封装,抛弃了作为背景存在(必不可少,但一般情况下不需要了解),占据了99%的体积和复杂度的操作系统层。应用容器成为了可以独立存储、独立操作的第一性的实体。轻装上阵的容器在性能、资源占用、可管理性等方面完全超越了虚胖的虚拟机。
- 差量相互作用:Docker容器之间通过精确受控的方式发生相互作用,可通过操作系统的namespace机制选择性的实现资源隔离或者共享。而虚拟机的差量切片之间是没有任何隔离机制的。
- 差量具有结构:虚拟机虽然支持增量备份,但是人们却没有合适的手段去主动构造一个指定的差量切片出来。归根结底,是因为虚拟机的差量定义在二进制字节空间中,而这个空间非常贫瘠,几乎没有什么用户可以控制的构造模式。而Docker的差量是定义在差量文件系统空间中,这个空间继承了Linux社区最丰富的历史资源。每一条shell指令的执行结果最终反映到文件系统中都是增加/删除/修改了某些文件,所以每一条shell指令都可以被看作是某个差量的定义。差量构成了一个异常丰富的结构空间,差量既是这个空间中的变换算符(shell指令),又是变换算符的运算结果。差量与差量相遇产生新的差量,这种生生不息才是Docker的生命力所在。
(二)React2013年,也就是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 的速度是非常快的)。整体策略可以表示为如下公式
显然,这一策略也是可逆计算的一种特例。 只要稍微留意一下就会发现,最近几年merge/diff/residual/delta等表达差量运算的概念越来越多的出现在软件设计领域中。比如大数据领域的流计算引擎中,流与表之间的关系可以表示为
对表的增删改查操作可以被编码为事件流,而将表示数据变化的事件累积到一起就形成了数据表。 现代科学发端于微积分的发明,而微分的本质就是自动计算无穷小差量,而积分则是微分的逆运算,自动对无穷小量进行汇总合并。19世纪70年代,经济学经历了一场边际革命,将微积分的思想引入经济分析,在边际这一概念之上重建了整个经济学大厦。软件构造理论发展到今天,已经进入一个瓶颈,也到了应该重新认识差量的时候。 四. 结语笔者的专业背景是理论物理学,可逆计算源于笔者将物理学和数学的思想引入软件领域的一种尝试,它最早由笔者在2007年左右提出。一直以来,软件领域对于自然规律的应用一般情况下都只限于"模拟"范畴,例如流体动力学模拟软件,虽然它内置了人类所认知的最深刻的一些世界规律,但这些规律并没有被用于指导和定义软件世界自身的构造和演化,它们的指向范围是软件世界之外,而不是软件世界自身。在笔者看来,在软件世界中,我们完全可以站在“上帝的视角”,规划和定义一系列的结构构造规律,辅助我们完成软件世界的构建。而为了完成这一点,我们首先需要建立程序世界中的“微积分”。 类似于微积分,可逆计算理论的核心是将“差量”提升为第一性的概念,将全量看作是差量的一种特例(全量=单位元+全量)。传统的程序世界中我们所表达的都只是“有”,而且是“所有”,差量只能通过全量之间的运算间接得到,它的表述和操纵都需要特殊处理,而基于可逆计算理论,我们首先应该定义所有差量概念的表达形式,然后再围绕这些概念去建立整个领域概念体系。为了保证差量所在数学空间的完备性(差量之间的运算结果仍然需要是合法的差量),差量所表达的不能仅仅是“有”,而必须是“有”和“没有”的一种混合体。也就是说差量必须是“可逆的”。可逆性具有非常深刻的物理学内涵,在基本的概念体系中内置这一概念可以解决很多非常棘手的软件构造问题。 为了处理分布式问题,现代软件开发体系已经接受了不可变数据的概念,而为了解决大粒度软件复用问题,我们还需要接受不可变逻辑的概念(复用可以看作是保持原有逻辑不变,然后增加差量描述)。目前,业内已经逐步出现了一些富有创造性的主动应用差量概念的实践,它们都可以在可逆计算的理论框架下得到统一的诠释。笔者提出了一种新的程序语言X语言,它可以极大简化可逆计算的技术实现。目前笔者已经基于X语言设计并实现了一系列软件框架和生产工具,并基于它们提出了一种新的软件生产范式(NOP)。
浏览器前端编程的面貌自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版本据说要做大的变革, 不知是要与历史决裂, 放弃兼容性, 还是继续挣扎, 在夹缝中求生.
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)技术的全部价值。
业务架构平台的设计与实现要比普通业务系统困难很多。一个核心难点在于如何建立普遍有效的应用程序模型,如何控制各种偶然性的业务需求对系统整体架构的冲击。大多数现有的业务架构平台都是提供了一个庞大的万能性产品,它预料到了所有可能在业务系统开发中出现的可能性,并提供了相应的处理手段。业务系统开发人员的能力被限定在业务架构平台所允许的范围之内。如果业务架构平台的复杂度为A+,则我们最多只能用它来开发复杂度为A的业务系统。一个典型的特征就是使用业务架构平台的功能配置非常简单,但是要开发相应的功能特性则非常困难,而且必须采用与业务系统开发完全不同的技术手段和开发方式。
采用业务架构平台来开发业务系统,即使看似开发工作量小,最终产生的各类配置代码量也可能会大大超过普通手工编程产生的代码量,这意味着平台封装了业务内在的复杂性,还是意味着平台引入了不必要的复杂性?很多业务架构平台的卖点都是零代码的应用开发,低水平的开发人员也可以主导的开发,但是为什么高水平的程序员不能借助于这些开发平台极大的提高生产率?
一般的业务架构平台无法回答以下问题:
1) 业务系统可以通过使用设计工具来重用业务架构平台已经实现的功能,但是业务系统内部大量相似的模型配置如何才能够被重用?
2) 特定的业务领域中存在着大量特殊的业务规则,例如“审批串行进行,每一步都允许回退到上一步,而且允许选择跳转到任意后一步”。这些规则如何才能够被引入设计工具,简化配置过程?
3) 已经开发好的业务系统作为产品来销售的时候,如何应对具体客户的定制化?如果按照客户要求修改配置,则以后业务系统自身是否还能够实现版本升级?
Witrix平台提供的基本开发模型为
App = Biz aop-extends Generator<DSL>
在这一图景下,我们就可以回答以上三个问题:
1) 业务模型通过领域特定语言(DSL)来表达,因此可以使用语言中通用的继承或者组件抽象机制来实现模型重用。
2) 推理机对于所有推理规则一视同仁,特殊的业务规则与通用的业务规则一样都可以参与推理过程,并且一般情况下特殊的业务规则更能够大幅简化系统实现结构。
3) 相对于原始模型的修改被独立出来,然后应用面向切面(AOP)技术将这些特定代码织入到原始模型中。原始模型与差异修改相互分离,因此原始模型可以随时升级。
Witrix平台所强调的不是强大的功能,而是一切表象之后的数学规律。Witrix平台通过少数基本原理的反复应用来构造软件系统,它本身就是采用平台技术构造的产物。我们用复杂度为A的工具制造复杂度为A+的产品,然后进一步以这个复杂度为A+的产品为工具来构造复杂度为A++的产品。
一种技术思想如果确实能够简化编程,有效降低系统构造的复杂性,那么它必然具有某种内在的数学解释。反之,无论一种技术机制显得如何华丽高深,如果它没有
清晰的数学图象,那么就很难证明自身存在的价值。对于模型驱动架构(MDA),我长期以来一直都持有一种批判态度。(Physical Model
Driven http://canonical.javaeye.com/blog/29412
)。原因就在于“由工具自动实现从平台无关模型(PIM)向平台相关模型(PSM)的转换”这一图景似乎只是想把系统从实现的泥沼中拯救出来,遮蔽特定语
言,特定平台中的偶然的限制条件,并没有触及到系统复杂性这一核心问题。而所谓的可视化建模充其量不过是说明人类超强的视觉模式识别能力使得我们可以迅速
识别系统全景图中隐含的整体结构,更快的实现对系统结构的理解,并没有证明系统复杂性有任何本质性的降低。不过如果我们换一个视角,
不把模型局限为某种可视化的结构图,而将它定义为某种高度浓缩的领域描述,
则模型驱动基本上等价于根据领域描述自动推导得到最终的应用程序。沿着这一思路,Witrix平台中的很多设计实际上可以被解释为模型定义,模型推导以及
模型嵌入等方面的探索。这些具体技术的背后需要的是比一般MDA思想更加精致的设计原理作为支撑。我们可以进行如下抽象分析。(Witrix架构分析 http://canonical.javaeye.com/blog/126467
)
1. 问题复杂?线性切分是削减问题规模(从而降低问题复杂性)的通用手段,例如模块(Module)。(软件中的分析学 http://canonical.javaeye.com/blog/33885
)
App = M1 + M2 + M3 +
2. 分块过多?同态映射是系统约化的一般化策略,例如多态(polymorphism)。(同构与同态:认识同一性 http://canonical.javaeye.com/admin/blogs/340704
)
(abc,abb,ade, ) -> [a], (bbb, bcd,bab, ) -> [b]
3. 递归使用以上两种方法,将分分合合的游戏进行到底,推向极致。
4. 以少控多的终极形态?如果存在,则构成输入与输出之间的非线性变换(输入中局部微小变化将导致输出中大范围明显的变化)。
App = F(M)
5.
变换函数F可以被诠释为解释器(Interpreter)或者翻译机,例如工作流引擎将工作流描述信息翻译为一步步的具体操作,工作流描述可以看作是由底
层引擎支撑的,在更高的抽象层面上运行的领域模型。但是在这种观点下,变换函数F似乎是针对某种特定模型构造的,引擎内部信息传导的途径是确定的,关注的
重点始终在模型上,那么解释器自身应该如何被构造出来呢?
App ~ M
6.
另外一种更加开放的观点是将变换函数F看作是生成器(Generator)或者推理机。F将根据输入的信息,结合其他知识,推理生成一系列新的命题和断
言。模型的力量源于推导。变换函数F本身在系统构造过程中处于核心地位,M仅仅是触发其推理过程的信息源而已。F将榨干M的最后一点剩余价值,所有根据M
能够确定的事实将被自动实现,而大量单靠M自身的信息无法判定的命题也可以结合F内在的知识作出判断。生成器自身的构造过程非常简单--只要不断向推理系
统中增加新的推理规则即可。语言内置的模板机制(template)及元编程技术(meta
programming),或者跨越语言边界的代码生成工具都可以看作是生成器的具体实例。(关于代码生成和DSL http://canonical.javaeye.com/blog/275015
)
App = G<M>
7. 生成器G之所以可以被独立实现,是因为我们可以实现相对知识与绝对知识的分离, 这也正是面向对象技术的本质所在。(面向对象之形式系统 http://canonical.javaeye.com/blog/37064
)
G<M> ==> G = {m => m.x(a,b,c);m.y(); }
8.
现实世界的不完美,就在于现实决不按照我们为其指定的理想路线前进。具体场景中总是存在着大量我们无法预知的“噪声”,它们使得任何在“过去”确立的方程
都无法在“未来”保持持久的平衡。传统模型驱动架构的困境就在于此。我们可以选择将模型M和生成器G不断复杂化,容纳越来越多的偶然性,直至失去对模型整
体结构的控制力。另外一种选择是模型在不断膨胀,不断提高覆盖能力的过程中,不断的空洞化,产生大量可插入(plugin)的接入点,最终丧失模型的推理
能力,退化成为一种编码规范。Witrix平台中采用的是第三种选择:模型嵌入--模型中的多余信息被不断清洗掉,模型通过精炼化来突出自身存在的合理
性,成为更广泛的运行环境中的支撑骨架。(结构的自足性 http://canonical.javaeye.com/blog/482620
)
App != G0<M0> , App != G0<M1>, App = G1<M1>
9.
现在的问题是:如何基于一个已经被完美解决的重大问题,来更有效率的搞定不断出现但又不是重复出现的小问题。现在我们所需要的不是沿着某个维度进行均匀的
切分,而必须是某种有效的降维手段。如果我们可以定义一种投影算子P,
将待解决的问题投射到已经被解决的问题域中,则剩下的补集往往可以被简化。(主从分解而不是正交分解 http://canonical.javaeye.com/blog/196826
)
dA = App - P[App] = App - G0<M0>
10. 要实现以上微扰分析策略,前提条件是可以定义逆元,并且需要定义一种精细的粘结操作,可以将分散的扰动量极为精确的应用到基础系统的各处。Witrix平台的具体实现类似于某种AOP(面向切面编程)技术。(逆元:不存在的真实存在 http://canonical.javaeye.com/blog/325051
)
App = A + D + B = (A + B + C) - C + D = App0 + (-C + D) = G0<M0> + dA
11. 模型驱动并不意味着一个应用只能由唯一的一个模型来驱动,但是如果引入多个不同形式的模型,则必须为如下推理提供具体的技术路径:
A. 将多个模型变换到共同的描述域
B. 实现多个模型的加和
C. 处理模型之间的冲突并填补模型之间的空白
在Witrix平台中模型嵌入/组合主要依赖于文本化及编译期运行等机制。(文本化 http://canonical.javaeye.com/blog/309395
)
App = Ga<Ma> + Gb<Mb> + dA
12.
系统的开发时刻t0和实施时刻t1一般是明确分离的,因此如果我们要建立一个包含开发与实施时刻信息的模型,则这一模型必须是含时的,多阶段的。关于时
间,我们所知道的最重要的事实之一是“未来是不可预知的”。在t0时刻建立的模型如果要涵盖t1时刻的所有变化,则必须做出大量猜测,而且t1时刻距离
t0时刻越远,猜测的量越大,“猜测有效”这一集合的测度越小,直至为0。延迟选择是实现含时系统控制的不二法门。
在Witrix平台中,所有功能特性的实现都包含某种元数据描述或者定制模板,因此结合配置机制以及动态编译技术既可实现多阶段模型。例如对于一个在未来
才能确定的常量数组,我们可以定义一个Excel文件来允许实施人员录入具体的值,然后通过动态编译技术在编译期解析Excel文件,并完成一系列数值映
射运算,最终将其转化为编译期存在的一个常量。这一过程不会引入任何额外的运行成本,也不要求任何特定的缓存机制,最终的运行结构与在未来当所有信息都在
位之后再手写代码没有任何区别。(D语言与tpl之编译期动作 http://canonical.javaeye.com/blog/57244
)
App(t1) = G(t0,t1)<M(t0,t1)> + dA(t0,t1)
13. 级列理论提供了一个演化框架,它指出孤立的模型必须被放在模型级列中被定义,被解释。(关于级列设计理论 http://canonical.javaeye.com/blog/33824
)
M[n] = G<M[n-1]> + dMn
14.
推理的链条会因为任何局部反例的出现而中断。在任意时空点上,我们能够断言的事实有哪些?信息越少,我们能够确定的事实越少,能够做出的推论也就越少。现
在流行的很多设计实质上是在破坏系统的对称性,破坏系统大范围的结构。比如明明ORM容器已经实现所有数据对象的统一管理,非要将其拆分为每个业务表一个
的DAO接口。很多对灵活性的追求完全没有搞清楚信息是对不确定性的消除,而不确定性的减少意味着限制的增加,约束的增加。(From Local To
Global http://canonical.javaeye.com/blog/42874
)
组件/构件技术的宣言是生产即组装,但是组装有成本,有后遗症(例如需要额外的胶水或者螺钉)。软件的本质并不是物质,而是信息,而信息的本质是抽象的规
律。在抽象世界中最有效的生产方式是抽象的运算,运算即生产。组件式开发意味着服从现有规律,熟练应用,而原理性生产则意味着不断创造新的规律。功能模
块越多,维护的成本越高,是负担,而推理机制越多,生产的成本越低,是财富。只有恢复软件的抽象性,明确把握软件构造过程内在的数学原理,才能真正释放软
件研发的生产力。(从编写代码到制造代码 http://canonical.javaeye.com/blog/333167
)
注解1:很多设计原则其实是在强调软件由人构造由人理解,软件开发本质上是人类工程学,需要关注人类的理解力与协作能力。例如共同的建模语言减少交互成本,基于模型减少设计与实现的分离,易读的代码比高性能的代码更重要,做一件事只有唯一的一种方式等。
注解2:生成系统的演绎远比我们想象的要深刻与复杂得多。例如生命的成长可以被看作是在外界反馈下不断调整的生成过程。
注解3:领域描述是更紧致但却未必是更本质的表达。人类的DNA如果表达为ATGC序列完全可以拷贝到U盘中带走,只要对DNA做少量增删,就可以实现老
鼠到人类的变换(人类和老鼠都有大约30000条基因,其中约有80%的基因是“完全一样的”,大约共享有99%的类似基因),但是很难认为人类所有智慧
的本质都体现在DNA中,DNA看起来更像是某种序列化保存形式而已。
注解4:模型转换这一提法似乎是在强调模型之间的同构对应,转换似乎总是可以双向进行的,仅仅是实现难度限制了反向转换而已。但是大量有用的模型变换却是单向的,变换过程要求不断补充新的信息。
注解5:模型驱动在很多人看来就是数据库模型或者对象模型驱动系统界面运行,但实际上模型可以意指任意抽象知识。虽然在目前业内广泛流行的对象范式下,所
有知识都可以表达为对象之间的关联,但是对象关系(名词之间的关联关系)对运算结构的揭示是远远不够充分的。很多时候所谓的领域模型仅仅是表明概念之间具
有相关性,但是如果不补充一大段文字说明,我们对于系统如何运作仍然一知半解。数学分析其实是将领域内在的意义抽空,仅余下通用的形式化符号。
html主要通过内置的<script>,<link>, <img>等标签引入外部的资源文件,一般的Web框架并没有对这些资源文件进行抽象,因此在实现组件封装时存在一些难以克服的困难。例如一个使用传统JSP Tag机制实现的Web组件中可能用到js1.js, js2.js和css1.css等文件,当在界面上存在多个同样的组件的时候,可能会生成多个重复的<script>和<link>标签调用,这将对页面性能造成严重的负面影响。资源管理应该是一个Web框架的内置组成部分之一。在Witrix平台中,我们主要借助于tpl模板引擎来输出html文本, 因此可以通过自定义标签机制重新实现资源相关的html标签, 由此来提供如下增强处理功能:
1. 识别contextPath
tpl模板中的所有资源相关标签都会自动拼接Web应用的contextPath, 例如当contextPath=myApp时
<script src="/a.js"></script> 将最终输出 <script src="/myApp/a.js" ...>
2. 识别重复装载
<script src="a.js" tpl:once="true"></script>
tpl:once属性将保证在页面中script标签实际只会出现一次.
3. 识别组件内相对路径
开发Web组件时,我们希望所有资源文件都应该相对组件目录进行定位,但是直接输出的<script>等标签都是相对于最终的调用链接进行相对路径定位的. 例如在page1.jsp中调用了组件A, 在组件A的实现中, 输出了<script src="my_control.js"></script>
我们的意图一般是相对于组件A的实现文件进行定位, 而不是相对于page1.jsp进行定位. tpl模板引擎的相对路径解析规则为永远相对于当前文件进行定位. 例如
<c:include src="sub.tpl" />
在sub.tpl中的所有相对路径都相对于sub.tpl文件进行定位.
4. 编译期文件有效性检查
在编译期, tpl引擎会检查所有引入的资源文件的有效性. 如果发现资源文件丢失, 将直接抛出异常. 这样就不用等到上线后才发现文件命名已修改等问题.
5. 缓存控制
浏览器缺省会缓存css, js等文件, 因此系统上线后如果修改资源文件可能会造成与客户端缓存不一致的情况. 一个简单的处理方式是每次生成资源链接的时候都拼接文件的修改日期或者版本号, 这样既可利用客户端缓存, 又可以保证总是使用最新版本. 例如
<script src="a.js"></script>将会输出 <script src="/myApp/myModule/a.js?344566" ...>
6. 字符集选择
为了简化国际化处理, 一般提倡的最佳实践方式是坚持使用UTF-8编码. 但是很多情况下可能使用系统内置的GBK编码会更加方便一些, 另外集成一些既有代码时也存在着不同字符集的问题. 在Witrix平台中, 所有输出的资源标签都会标明对应的字符集, 如果没有明确设置就取系统参数中的缺省字符集.
例如 <script src="a.js"></script> 将会输出 <script ... charset="GBK"></script>
7. 缺省theme支持
为了支持多种页面风格, 往往不是简单的替换css文件即可实现的, 它可能意味着整个组件的实现代码的更换. Witrix平台中通过一系列缺省判断来简化这一过程. 例如如下代码表明如果设置了ui_theme系统参数, 并且对应的特殊实现存在, 则使用特殊实现, 否则系统缺省实现.
<c:include src="${cp:ui_theme()}/ctl_my_ctl.tpl" >
<c:include src="default/ctl_my_ctl.tpl" />
</c:include>
AOP(Aspect Oriented Programming)早已不是什么新鲜的概念,但有趣的是,除了事务(transaction), 日志(Log)等寥寥几个样板应用之外,我们似乎找不到它的用武之地。http://canonical.javaeye.com/blog/34941
很多人的疑惑是我直接改代码就行了,干吗要用AOP呢?AOP的定义和实现那么复杂,能够提供什么特异的价值呢?
Witrix平台依赖于AOP概念来完成领域模型抽象与模型变换,但是在具体的实现方式上,却与常见的AOP软件包有着很大差异。http://canonical.javaeye.com/blog/542622
AOP的具体技术内容包括定位和组装两个部分。简化切点定位方式和重新规划组装空间,是Witrix中有效使用AOP技术的前提。
在Witrix平台中,对于AOP技术的一种具体应用是支持产品的二次开发。在产品的实施过程中,经常需要根据客户的特定需求,修改某些函数的实现。我们
可以选择在主版本代码中不断追加相互纠缠的if-else语句,试图去包容所有已知和未知的应用场景。我们也可以选择主版本代码和定制代码独立开发的方
式,主版本代码实现逻辑框架,定制代码通过AOP机制与主版本代码融合,根据具体场景要求对主版本功能进行修正。AOP的这种应用与所谓的横切概念是有所
区别的。典型的,一个横切的切点会涉及到很多类的很多方法,而函数定制则往往要求准确定位到某个业务对象的某个特定的业务方法上。传统AOP技术的切点定
义方式并不适合这种精确的单点定位。在Witrix平台中,我们通过直接的名称映射来定义切点。例如,修正spring中注册的MyObject对象的
myFunc方法,可以在app.aop.xml文件中增加如下标签
<myObject.myFunc>
在原函数执行之前执行
<aop:Proceed/> <!-- 执行原函数内容 -->
在原函数执行之后执行
</myObject.myFunc>
[spring对象名.方法名]这种映射方法比基于正则字符串匹配的方式要简单明确的多。spring容器本身已经实现了对象的全局管理功能,spring对象名称必然是唯一的,公开发布的,相互之间不冲突的,没有必要再通过匹配运算重新发现出它的唯一性。
对于一些确实存在的横切需求,我们可以通过Annotation机制来实现切点坐标标定,将复杂的切点匹配问题重新划归为[对象名.方法名]。
@AopClass({"myObject","otherObject"})
class SomeClass{
@AopMethod({"myFunc","otherFunc"})
void someFunc(){}
}
针对以上对象,在app.aop.xml文件中可以定义
<I-myObject.I-myFunc>
.
</I-myObject.I-myFunc>
结构的稳定性,直观的理解起来,就是结构在存在外部扰动的情况下长时间保持某种形式不变性的能力。稳定意味着小的扰动造成的后果也是“小”的。在数学中,Taylor级数为我们描绘了变化传播的基本图景。
F(x0 + dx) = F(x0) + F'(x0)*dx + 0.5*F''(x0)*dx^2 + ![](http://www.blogjava.net/Images/dot.gif)
扰动dx可能在系统F中引发非常复杂的作用过程,在系统各处产生一个个局部变化结果。表面上看起来,似乎这些变化结果存在着无穷多种可能的分组方式,例如 (F'(x0)-2)*dx + 2*dx^2, 但是基于微分分析,我们却很容易了解到Taylor级数的每一级都对应着独立的物理解释,它们构成自然的分组标准。某一量级下的所有变化汇总归并到一起,并对应一个明确的整体描述。在抽象的数理空间中,我们具有一种无所不达的变化搜集能力。变化项可以从基础结构中分离出来,经过汇总后可以对其进行独立的研究。变化本身并不会直接导致基础结构的崩溃。
在软件建模领域,模型的稳定性面临的却是另一番场景。一个软件模型一旦被实现之后,种种局部需求变更就都会形成对原有基础结构的冲击。一些局部的需求变化可能造成大片原有实现失效,我们将被迫为类似的需求重新编写类似的代码。此时,软件开发并不像是一种纯粹的信息创造,而是宛若某种物质产品的生产(参见从编写代码到制造代码 http://canonical.javaeye.com/blog/333167 )。显然,我们需要一种能力,将局部变化从基础结构中剥离出来,经过汇总归并之后再进行综合分析和处理。这正是AOP(Aspect Oriented Programming)技术的价值所在。
M1 = (G0+dG0)<M0+dM0> ==> M1 = G0<M0> + dM
AOP本质上是软件结构空间的自由修正机制。只有结合AOP技术之后,软件模型才能够重新恢复抽象的本质,在时间之河中逃离随机变化的侵蚀,保持实现层面的稳定性。在这一背景下,建模的目的将不是为了能够跟踪最终需求的变动,而是要在某个独立的层面上能够自圆其说,能够具有某种独立存在的完满性,成为思维上可以把握的某个稳定的基点。模型的真实性将因为自身结构的完备性而得到证明,与外部世界的契合程度不再是价值判断的唯一标准。 http://canonical.javaeye.com/blog/482620
说到软件建模,一个常见的论调是模型应该符合实际需求,反映问题的本质。但是何谓本质,却是没有先验定义的。在成功的建立一个模型之前,无论在内涵上还是在外延上我们都很难说清楚一个问题的本质是什么。如果将模型看作是对领域结构的一种显式描述和表达,我们可以首先考察一下一个“合适”的结构应该具备哪些特征。
按照结构主义哲学的观点,结构具有三个要素:整体性,具有转换规律或法则(转换性),自身调整性(自律性)。整体性意味着结构不能被简单的切分,其构成要素通过内在的关系运算实现大范围的关联与转换,整体之所以成为整体正是以转换/运算的第一性为保证的。这种转换可以是共时的(同时存在的各元素),也可以是历时的(历史的转换构造过程),这意味着结构总要求一个内在的构造过程,在独立于外部环境的情况下结构具有某种自给自足的特性,不依赖于外部条件即可独立的存在并保持内在的活动。自律性意味着结构内在的转换总是维持着某种封闭性和守恒性,确保新的成分在无限地构成而结构边界却保持稳定。注意到这里对结构的评判并不是来自外在规范和约束,而是基于结构内在的规律性,所强调的不是结构对外部条件的适应性,而是自身概念体系的完备性。实际上,一个无法直接对应于当前实际环境的结构仍然可能具有重要的价值,并在解决问题的过程中扮演不可或缺的角色。在合理性这个视角下,我们所关注的不仅仅是当前的现实世界,而是所有可能的世界。一个“合理”的结构的价值必能在它所适应的世界中凸现出来。
在信息系统中,我们可能经常会问这个模型是否是对业务的准确描述,是否可以适应需求的变更,是否允许未来的各种扩展等等。但是如果换一个思维方向,我们会发现这些问题都是针对最终确立的模型而发问的,而在模型构建的过程中,那些可被利用的已存在的或者可以存在的模型又是哪些呢。每一个信息模型都对应着某种自动推理机,可以接收信息并做一定的推导综合工作。一个可行的问题是,如何才能更有效的利用已有的信息进行推导,如何消除冗余并减少各种转换成本。我们经常可以观察到,某一信息组织方式更充分的发掘了信息之间的内在关联(一个表象是它对信息的使用不是简单的局域化的,而是在多处呈现为互相纠缠的方式,难以被分解),这种内在关联足够丰富,以至于我们不依赖于外部因素就可以独立的理解。这种纠缠在一起的信息块自然会成为我们建模的对象。
如果模型的“覆盖能力”不再是我们关注的重点,那么建模的思维图式将会发生如下的转化
最终的模型可以由一系列微模型交织构成。模型的递进构造过程并不同于组件(Component)的实物组装接口,也不是CAD图纸堆叠式的架构概念所能容纳的。在Witrix平台中,模型分解和构造表达为如下形式 http://canonical.javaeye.com/blog/333167
Biz[n] = Biz[n+1] aop-extends CodeGenerator<DSLx, DSLy>。
在软件发展的早期,所有的程序都是特殊构造的,其必然的假设是【在此情况下】,重用不在考虑范围之内,开发可以说是一个盲目试错的过程。随着我们逐步积累了一些经验,开始自觉的应用理论分析手段,【在所有情况下】都成立的一些普适的原理被揭示出来,它们成为我们在广阔的未知世界中跋涉时的向导。当我们的足迹渐渐接近领域的边界,对领域的全貌有一个总体的认知之后,一种对自身成熟性的自信很自然的将我们导向更加领域特定的分析。很多时候,我们会发现一个特化假设可以大大提高信息的利用率,推导出众多未被显式设定的知识。我们需要那些【在某些情况下】有效的规则来构成一个完备的模型库。这就如同有大量备选的数学定理,面对不同的物理现象,我们会从一系列的数学工具中选择一个进行使用一样。
软件开发技术的技术本质在于对代码结构的有效控制. 我们需要能够有效的分解/重组代码片断, 凸显设计意图. 面向对象是目前最常见的代码组织技术. 典型的, 它可以处理如下模式
A1 --> B2, A2 --> B2, A3 --> B3 ...
我们观察到A1和A2之间, B2和B2之间具有某种概念关联性, 同时存在某种抽象结构 [A] --> [B].
对于这种情况, 我们可以定义对象 [A], [B], 它们分别是 A1和A2的聚合, B1和B2的聚合等. 举例来说, 对于如下表格描述, <ui:Col>所提供的信息在映射为html实现的时候将在多处被应用.
<ui:Table data="${data}">
<ui:Col name="fieldA" label="labelA" width="20" />
<ui:Col name="fieldB" label="labelB" width="10" />
</ui:Table>
这里<ui:Col>提供的信息对应三个部分的内容: 1. 列标题 2. 列样式(宽度等) 3. 列数据
面向对象的常见做法是抽象出 UiCol对象, 它作为UiTable对象的属性存在, 在生成表头, 表列样式和表格数据内容时将被使用. 但是我们注意到面向对象要求多个方法通过this指针形成状态耦合
,在某种意义上它意味着所有的成员方法在任一时刻都是同时存在着的。它们所代表着的存在的代价必须被接受(存储空间等)。即使并不同时被使用,我们仍然需要同时持有所有成员函数指针及
共享的this指针。实际上, 我们并不一定需要A1和A2同时在场. 在这种情况下, 编译期技术可以提供另一种不同的行为聚合方式.
<table>
<thead>
<sys:CompileTagBody cp:part="thead" />
</thead>
<cols>
<sys:CompileTagBody cp:part="cols" />
</cols>
<tbody>
<sys:CompileTagBody cp:part="tbody" />
</tbody>
</table>
只要<ui:Col>标签的实现中针对编译期的cp:part变量进行分别处理, 即可实现信息的部分析取.
html最早的设计目标只是作为某种多媒体文档展现技术,其设计者显然无法预料到今天Web应用的蓬勃发展,一些设计缺陷也就难以避免。特别是html规范中缺乏对于复杂交互式组件模型的支持,直接导致企业应用的前台开发困难重重。AJAX技术可以看作是对这种困境的一种改良性响应,它试图通过javascript语言在应用层创建并维护一系列复杂的交互机制。很多完善的ajax框架走得相当遥远,最终基本将html作为一种底层“汇编”语言来使用。例如,一个很整齐美观的类Excel表格可能是由一个个div拼接而成,与html原生的table元素已经没有任何关系。
Witrix平台中对于前台html模型也作了一定的增强,但基本的设计思想是尽量利用原生控件,并尽量保持原生控件内在的数据关系,而不是重新构建一个完整的底层支撑环境。采用这种设计的原因大致有如下几点:
1. 前台技术目前竞争非常激烈,我们优先选择的方式是集成第三方组件,尽量保持原生环境有利于降低集成成本。
2. 通过javascript构造的控件可能存在性能瓶颈和其他浏览器内在的限制。例如一般Ajax框架提供的Grid控件都无法支撑大量单元格的显示。
3. Witrix平台的tpl模板技术可以非常方便的生成html文本,并提供强大的控件抽象能力,因此在前台动态创建并组织界面元素在Witrix平台中是一种不经济的做法。
4. Witrix平台提供的分解机制非常细致,存储于不同地方的不同来源的代码会在不同的时刻织入到最终的页面中,基于原生环境有利于降低平台快速演进过程中的设计风险。
Witrix平台中对于html模型的增强主要关注于以最少的代码实现界面控件与业务逻辑的自然结合。基本结构包括:
1. 通过ControlManager对象在前台建立一种container结构,统一管理控件的注册和获取。js.makeControl(elmOrId)返回特殊注册的控件对象或者根据原生html元素生成一个包装对象。
2. 通过js.getWxValue(elm)和js.setWxValue(elm,value)这两个函数统一对控件的值的存取过程。
3. 通过js.regListener(elm,listenerFunc)统一管理控件之间的相关触发,实现控件之间的相互监听。当js.setWxValue(elm,value)被调用时,注册在ControlManager中的listenerFunc将被调用。
4. stdPage.setFieldValue(fieldName,value)和stdPage.getFieldValue(fieldName,value)统一针对业务字段的值的存取过程,这里fieldName对应于实体上的业务字段名。
5. 通过ajax.addForm(frmId)等函数统一前台提交参数的提取过程,通过stdPage.buildAjax()等函数统一后台服务的调用方式。
6. 通过stdPage对象统一封装业务场景中的"常识"。
基于以上一些基础机制,Witrix平台即可提供一些复杂的业务组件封装。例如<input name="productCode" onkeypress="stdPage.keyPressToLoadRefByCode({objectName:'SomeProduct',queryField:'productCode'})" .../>通过简单的调用一个js函数即可实现如下功能:
a. 在文本框中输入回车的时候自动提交到后台查找对应产品代码的产品,并更新前台多个相关字段的值
b. 如果没有查找到相应产品,则弹出对话框根据界面上已有的部分字段信息提示客户添加新的产品信息。
c. 如果找到多个对应产品,则弹出列表允许客户选择其一。
d. 具体的处理过程可以通过函数参数进行精细的控制。
在meta文件中,结合上下文环境中的元数据信息,我们在缺省情况下可以直接使用 <ds:LoadRefByCodeInputor objectName="SomeProduct" />标签,不需要任何其他附加参数。
Witrix平台中一般利用原生控件来保存数据值,而不是将数据保存在分离的js对象中。例如对于一个选择控件,经常要求选择得到的是实体的id,而显示在界面上的是某个其他字段的值。Witrix平台中一般的实现结构是
<input type="hidden" name="${fieldName}" value="${entity[dsMeta.idField]}" id="${id}" textId="text_${id}" />
<input type="text" value="${entity[dsMeta.nameField]}" id="text_${id}" />
通过textId等扩展属性即可明确定义控件多个部分之间的关联关系,同时保证控件的实现完全与html规范相兼容。
Witrix平台中目前使用的"标准化"的扩展属性有textId(对应文本显示控件的id), showName(某些无文字显示的选择控件需要保留显示字段值), op(字段作为查询条件提交时的比较算符),validator(字段值对应的检验函数),setWxValue/getWxValue(重定义控件值的存取行为),serializer(特殊处理前台控件的提交参数)等。扩展属性不仅可以引入说明信息,还可以引入丰富的控件行为。
分层是最常见的软件架构方式之一。分层之后可以区分出横纵两个维度,纵向往往表现出一种隔离性。出于有意无意的各种原因,层次之间传递信息很容易出现模糊甚至丢失的现象。B/S多层体系架构下的程序因为浏览器和服务器之间的状态空间相互独立,相对于共享全局状态空间的C/S程序,更容易出现信息传递不畅的问题。实际上,我们经常可以观察到B/S程序中存在着大量的"接力"代码,即在交界处,总是存在着大量用于读取变量,拼接变量,转换变量等与主体业务无关但却又不可或缺的代码。在多层架构程序中,信道构建应该是一个需要给予足够重视的问题。
在系统规划中,多层结构应该内置与具体语义无关的通用信道,它跨越多个层次,允许信息透明的通过,并以未预期的方式在不同的层面激发各种相关的行为。在Witrix平台中,平台代码与特定应用中的业务代码处于高度交织的状态,一个特定业务功能的实现往往需要多处业务代码相互协同,平台必须成为某种透明的背景。例如,假设我们编制了一个通用的列表选择控件,它封装的逻辑是从一个实体列表中进行选择
<app:SelectOne objectName="MyEntity" />
如果现在要求选择时只列出某个类型的实体,则调用形式为
<app:SelectOne objectName="MyEntity" extArgs="$bizId=select&$type=1" />
在调用入口处补充必要的信息之后会推动系统在遥远的状态空间中应用一个特定的过滤条件。这里$bizId负责指示平台应用特定的元数据配置,而其他的参数则由元数据中的逻辑负责处理。平台与特定业务代码各取所需,相互配合,将尽可能多的逻辑剥离为通用机制。
现代数学是建立在等价类这一概念的基础之上的。同构是对等价关系的一种刻划。简单的可以把它理解为两个系统之间的一种“保持”运算规则的一一对应关系。在
数学中一个符号所代表的是所有能够互相同构的对象。例如整数3可以看作是与某个元素个数为3的集合可以建立一一对应关系的所有的集合所构成的整体。所以在
数学中,如果我们解决某个特定的问题,它同时也就意味着我们解决了一系列相互等价的问题。
同构关系对于认知可以起到本质上的简化作用。如果通过一个推理链条,确认了A == B == C == D,则可以直接从概念上推导出 A
== D,
这一关系有可能被直观理解,而不需要理会中间的推理步骤。(注意到以上元素两两建立同构关系的时候可能要采用不同的对应手段,因此上面的等式并不是平凡
的。)另一方面,我们可以确定一个模型元素M, 将系统简化为 A == M, B == M, C == M, D ==
M。只要理解了元素M就理解了等价的其他元素。
Witrix平台中PDM定义作为基础的结构模型,它同时映射成为数据库表,以及hbm, java,
meta等多个代码文件,此外还对应于约定的WebObject名称和BizFlow文件名称,相应的报表文件目录等。我们只要理解了pdm模型,即可通
过推理自然的掌握各个层面上对应的结构。这意味着只要知道实体名称,就知道如何通过Web访问这个对象,知道数据在数据库中对应的数据库表,而不需要知道
后台是如何读取前台提交的参数以及如何执行保存数据指令的。不仅仅是在模型驱动领域,在系统设计的各个方面,我们都应该尽量充分的利用当前的信息通过推理
得到系统其他部分的结构,而不是通过手工关联或者判断在程序中动态维持这种对应关系。例如在flow-cp机制中,biz的id,
action的id等都根据step配置的id推导得到,这样在工作列表跳转的时候就可以根据规则推导出跳转页面对应的链接,而不需要手工编写页面重定向
代码。
![](/images/blogjava_net/canonical/mapping.jpg)
同态(homomorphism)关系相对于同构关系,只强调单向映射的可行性,它是一个舍弃属性的过程。同态作为最基础的认知手段之一,它不仅仅是用一
个符号来置换一组元素,而是同时保留了某种全局的运算关系,因此同态映像可以构成某种独立的完整的研究对象。通过同态映射,我们可以在不同的抽象层面上研
究原系统的一个简化版本。例如meta中的layout是一种典型的领域特定语言(DSL)。
userName userTitle
emailAddress
每一个字段表示了一个可能任意复杂的inputor或者viewer,
字段之间的前后关系描述了最终显示页面上显示内容的相对关系。当viewer根据需求发生改变的时候,并不影响到layout层面上的关系,因此
layout可以保持不变。如果我们在系统中把问题分解为多个抽象层面上,多个观察视角上的同态模型,则可能实现更高的软件复用程度。
在Witrix平台的设计中,很多细粒度的标签都定义为tpl文本段,这样平台只要理解某一层面上的交互关系,实际应用中可能出现的细节在标签内部进行局
部处理,不会突破原始设计的形式边界,不会影响到原先设定的主体系统结构。例如BizFlow中的tpls段,action的source段等。
上世纪50年代以前,生物学家做梦也想象不到具有无限复杂性的生物遗传过程,竟然可以被抽象为ATGC四个抽象符号的串联。生命竟然不理会各种已知的或是
未知的物理化学作用,被抽象的三联码所驱动。一种抽象的本质似乎成了生命世界的本原。在软件的世界中,可以被识别的抽象元素绝不只是语言本身所提供的那些
机制。
有一个心理学实验,要求被试者将青草,公鸡,牛三个东西分
成两组,结果多数中国儿童将青草和牛分成一组,而多数美国儿童将公鸡和牛分成一组。中国人的思想中青草和牛之间存在现实的关系,牛吃草,而西方人的典型逻
辑是公鸡和牛都属于动物这一范畴。通过分类将物体类型化,这是西方人从小就接受的训练。据说美国婴儿学习名词的速度要快于动词,而中国的婴儿则相反,这并不是偶然的。
中国人的传统哲学认为世界是普遍联系的,事物之间存在着祸福相依的辩证转化关系。而古希腊人强调个体意识,以两分法看待世界,他们将世界看成是孤立的物体组成(原子论)构成,然后选择一个孤立物体(脱离背景),开始研究它的各项属性,接着将属性泛化,构成分类的基础。西方语言中大量抽象概念都是从作为属性的形容词直接转化而来,例如
white
-->
whiteness
。而中文中很少有精确的类型定义,而多半是富有表现力的,隐喻性的词语,例如我们不谈论抽象的白,而只说雪白,没有抽象的
size
,而只说具体的大小。
亚里士多德认为铁球在空气中下落是因为它具有“重性”,而木块在水中漂浮是因为木块具有“轻性”。这种将一切原因归结为事物内在属性的传统在一定程度上妨碍了西方人认识到背景的存在和作用,但使得他们可以把问题简化。
古希腊人对于类型的热衷源于他们对于永恒的迷恋。静态的亘古不变的世界才是他们的思想栖息的场所。具体的物体是易逝的,多变的,只有抽象的类型才是永恒的存在,也只有抽象概念之间的关系才是永真的联系。而具体实例之间的关联在某种程度上被认为是不重要的,甚至是不可靠的。
![](/images/blogjava_net/canonical/class.jpg)
将具有某一属性的所有物体定义为一个集合,这一做法在上世纪初被发现会引起逻辑悖论,动摇了整个数学的基础,它绝不像表面上看起来那么单纯。但确定无疑的是,通过类型来把握不变的事实是一种非常重要且有效的认识策略。面向对象语言强调名词概
念,从引入类定义以及类之间的继承关系开始,这符合西方一贯的作风。而
Ruby
这种强调实例间关系的动态语言首先由日本人发明,可能也不是偶然的。虽然现在大家都在玩高科技了,可实际贩卖给你的多半仍然是包治百病的祖传秘方。文化可能造成认知上的一种偏执,在技术领域这一现象并没有被清楚的意识到。
软件开发作为一种工程技术,它所研究的一个重点就是如何才能有效降低软件产品的研发成本。在这一方向上,组件技术取得了空前的成功。它所提供的基本图景是
像搭积木一样从无到有的组装出最终的产品。在某种意义上,这是对现代建筑工业的模仿和致敬。新材料,预制件,框架结构,这些建筑学的进展在软件领域被一一
复制,建筑工地上的民工自然也成为了程序员们学习的楷模。毕竟,在组件的世界中码代码,基本上也是一种“搬砖”的行为。
值得庆幸的是,软件开发作为一种智力活动,它天生具有一种“去民工化”的倾向。信息产品有着与物质世界产品完全不同的抽象本质。在物理空间中,建造100
栋房屋,必须付出100倍的努力,老老实实的干上100遍。而在概念空间中建造100栋房屋,我们却可以说其他房屋与第一栋一模一样,加点平移旋转变换即
可。一块砖填了地基就不能用来盖屋顶,而一段写好的代码却可以在任何可用的场合无损耗的被使用。一栋建好的房屋发现水管漏水要大动干戈,而在完成的软件中
补个局部bug却是小菜一碟。在抽象的世界中有效的进行生产,所依赖的不应该是大干,苦干的堆砌,而应该是发现某种可用于推导的原理,基于这些原理,输入
信息可以立刻转换为最终的结果,而不需要一个逐步构造的过程。即我们有可能超越组装性生产,实现某种类似于数学的原理性生产。http://canonical.javaeye.com/blog/325051
代码复用是目前软件业中鼓吹降低生产成本的主要口号之一。但是在组件技术的指向下,一般所复用的只是用于构建的砖块,而并不是某种构造原理。即使在所有信
息已经确定的情况下,我们仍然不可能从需求立刻得到可执行的产品。很多代码即使我们在想象中已经历历在目,却仍然需要一行行把它们誊写下来。当我们发现系
统中已经没有任何组件值得抽象的时候,仍然留下来众多的工作需要机械化的执行。代码复用的理想国距离我们仍然非常的遥远。
子例程(subroutine)是最早的代码重用机制。这就像是将昨天已经完成的工作录制下来,在需要的时候重新播放。函数(function)相对于子
例程更加强大。很多代码看起来并不一样,但是如果把其中的差异部分看作是变量,那么它们的结构就可以统一了。再加上一些判断和循环,很多面目迥异的东西其
实是存在着大量共享信息的。面向对象技术是一次飞跃性的发展。众多相关信息被打包到一个名称(类型)中,复用的粒度和复杂度都大大提升。派生类从基类继
承,可以通过重载实现对原有代码的细致调整。不过,这种方式仍然无法满足日益增长的复用需求。很多时候,一个名称并不足以标定我们最终需要的代码结构,在
实际使用的时候还需要补充更多的信息。类型参数化,即泛型技术,从事后的角度看其实是一种明显的解决方案。根据参数动态的生成基类自然可以吸纳更多的变
化。经历了所谓Modern
C++的发展之后,我们现在已经非常明确,泛型并非仅仅能够实现类型共变,而是可以从类型参数中引入更丰富的结构信息,它的本质是一种代码生成的过程。http://canonical.javaeye.com/blog/57244
认清了这一点,它的扩展就非常明显了
BaseClass<ArgClass> --> CodeGenerator<DSL>
DSL(或者某种模型对象)相对于普通的类型(Class),信息密度要大很多,它可以提供更丰富也更完整的输入信息。而CodeGenerator也不必拘泥于基础语言本身提供的各种编译机制,而是可以灵活应用各种文本生成技术。http://canonical.javaeye.com/blog/309395
CodeGenerator在这里所提供的正是根据输入模型推导出完整实现代码的构造原理。
现在很多人热衷于开发自己的简易代码生成工具,这些工具也许可以在简单的情形下减轻一些体力工作,但是生成的代码一般不能直接满足需求,仍然需要手工执行
大量的删改工作。当代码生成之后,它成为一种固化的物质产品,不再能够随着代码生成工具的改进而同步改进,在长期的系统演化过程中,这些工具并不一定能够
减少累积的工作量。
修正过程 ==> CodeGenerator<DSL>
为了改进以上代码生产过程,一些人试图在CodeGenerator中引入越来越多的可配置性,将各种变化的可能内置在构造原理中。显然这会造成CodeGenerator的不正常的肿胀。当更多的偶然性被包含在原理中的时候,必然会破坏原理的简单性和普适性。
输入信息 + 一段用于推导的原理 + 修正补充 = 真实模型
必须存在[修正补充]这一项才能维持以上方程的持久平衡。
为了摆脱人工修正过程,将模型调整纳入到概念世界中,我们需要超越继承机制的,更加强大的,新的技术手段。其实在当前的技术背景下,这一技术已经是呼之欲出了。这就是AOP, Aspect Oriented Programming。http://canonical.javaeye.com/blog/34941
Biz ==[AOP extends]==> CodeGenerator<DSL>
继承仅仅能够实现同名方法之间的简单覆盖,而AOP所代表的技术原理却是在代码结构空间中进行任意复杂的删改操作,它潜在的能力等价于人工调整。
为了实现上述生产模式,需要对编程语言,组件模型,框架设计等方面进行一系列改造。目前通用的AOP实现和元编程技术其实并不足以支持以上模式。http://canonical.javaeye.com/blog/275015
这一生产模式将会如何演化,也是一个有趣的问题。按照级列理论,我们立刻可以得到如下发展方向:
Context0 + DSL1 + EXT0 = DSL0
Context1 + DSL2 + EXT1 = DSL1
http://canonical.javaeye.com/blog/33824
Witrix平台中BizFlow可以看作是对DaoWebAction的修正模型,但是它本身具有完整的意义,可以直观的被理解。在BizFlow的基础上可以逐步建立SeqFlow,StateFlow等模型。http://canonical.javaeye.com/blog/126467
现在有些人试图深挖函数式语言,利用模式匹配之类的概念,做符号空间的全局优化。但是我们需要认识到通用的机制是很少的,能够在通用语言背景下被明确提出
的问题更是很少的。只有在特定领域中,在掌握更多局部信息的情况下,我们才能提出丰富的问题,并作出一定背景下的解答。DSL的世界中待做的和可做的工作
很多。http://canonical.javaeye.com/blog/147065
对于程序员而言,未来将变得越来越丰富而复杂,它将持续拷问我们的洞察力。我们不是一行行的编写代码,把需求一条条的翻译到某种实现上,而是不断发明局部的生产原理,依靠自己制定的规则在抽象的空间中不断的创造新的表象。
负数没有直接的几何意义,因此它被认为是对应于不存在的事物。而按照古希腊的逻辑,不存在的事物是不可能存在的,因而也就是无法被理解的,更不可能参与到
推理过程中,因此是无意义的,无法被定义的,
因此它是不存在的。中国人注重的是运算的合理性,而不是数的真理性,大概在公元前400年左右就创造了负数和零的概念。而在西方直到公元7世纪(唐代)的
一本印度人的著作中才出现负数,它被用来表示负债。西方人没有能够创造负数,他们对负数的接受更迟至15世纪左右。这件事实在一定程度上说明了存在某种深
刻的困难阻碍我们理解负数概念。
在引入负数之前,3x^2 + 8 = 4x 和 3x^2 + 4x + 8 = 0
这两个方程的结构是完全不同的,它们需要不同的求解技术,因此也就不可能利用符号抽象出 a x^2 + b x + c =
0。引入负数才使得我们能够以统一的方式提出问题,并研究通用的求解技术。
群论(Group Theory)是对结构进行抽象研究的数学分支。群的定义包括四条规则
1. 元素之间的运算满足结合律 (a * b) * c = a * (b * c)
2. 元素之间的运算封闭,即 a * b 仍然属于该群
3. 存在单位元,即对所有a, a * e = e*a = a
4. 每个元素存在对应的逆元,a * a^-1= e
逆运算是非常重要的结构要求,逆元是对负数的一种抽象推广。如果没有逆元,则只能构成半群(semi-group),它的性质要少很多。
目前软件设计中所有的原则都指向组装过程,从无到有,层层累进。构件组装的隐喻中所包含的图像是操纵实际可见的积木,是缺少逆元概念的。
考察一个简单的例子,假设需要的产品是三角形内部挖去一个五边形的剩余部分。有三种生产策略:
1. 对最终需要的产品形态进行三角剖分,使用8个小三角形拼接出来。这种方式比较繁琐,而且最后粘接工序的可靠性和精确性值得怀疑。
2. 拿到一个真实的三角形,然后用刀在内部挖出一个五边形的洞。这种方式需要消耗一定的人工,而且也很难保证五边形的精确性,即使我们曾经精确的生产过其他五角形和三角形。实际上一般情况下我们是逐步锉出一个五边形的,并没有充分利用到五边形的对称性。
3. 在概念空间中做一个抽象计算 (-五边形) + (三角形) = 所需产品
如果我们能够生产一种负的五边形和一种正的三角形,则可以立刻得到最终的产品。
![](/images/blogjava_net/canonical/principle.jpg)
在软件开发的实践中,我们目前多数采用的是两种方式:
1. 采用可视化设计工具通过拖拽操作开发出完整的界面和后台
2. 拷贝一些已有的代码,删除掉不需要的部分,增加一些新的实现,也可能对已有实现做一些不兼容的修正。
在第二种方式中
新结构的构造 = 已有结构 + 软件之外的由人执行的一个剪裁过程
这个剪裁过程表现为一个时间序列。如果我们对原有结构进行了调整,则需要重新关联一个时间序列,而此时间序列并不会自动重播。为了压缩以时间为度量单位的
生产成本,我们必须减少对时间序列的依赖。在时间序列中展开的一个构造过程可以被转化为一个高维设计空间中的一种更加丰富的构造原理,我们最终的观测可以
看作是设计空间向物理空间的一个投影(想象一束光打下来)。这种方式更容易保证程序的正确性。
时间序列 --[原理转化]--> 空间关系
这样我们就可以使用第三种生产策略:利用构造原理进行抽象计算。如果我们只盯着产品的最终形态看,只是想着怎么把它像搭积木一样搭建出来,就不可能识别出
系统结构本身所蕴含的对称性。如果我们发现了系统内蕴的结构特征,但是却只能通过构造过程中的行动序列来追随它,同样无法实现有效的工作复用。同时因为这
个行动序列一般处于系统规则约束之外,完全由人的自觉来保障,因此很难保证它的正确性。现实世界的规范要求并不是模型本身所必须满足的,只要我们能够创造
新的结构原理,在概念空间中我们就可以拥有更多的自由。现在业内鼓吹的软件构造原理多半是参照物理世界中生产具体物质产品的生产工序,却没有真正把握信息的抽象本质。掌握规则,制订规则,才是信息空间中的游戏规则。
物理学中最重要的分析学思想之一是微扰论(Perturbation).
针对一个复杂的物理现象,首先建立一个全局的规范的模型,然后考虑各种微扰条件对原有模型的影响。在小扰动情况下,模型的变化部分往往可以被线性化,被局
域化,因而问题得到简化。微扰分析得到的解依赖于全局模型的解而存在,因而这是一种主从关系的分解方式。但是如果主体模型是我们已经熟知的物理现象,则我
们关注的重点可以全部放在扰动解上,认为所有特定的物理规律都体现在扰动解中。如果微扰分析得到的物理元素足够丰富,则微扰模型本身可以成为独立的研究对
象,在其中我们同样可以发现某种普适的结构规律。
考察如下的构造过程
X = a + b + c
Y = a + b + d = (a + b + c) - c + d = X - c + d
对于数学而言,上述的推导是等价的,但是对于物理学而言,Y = a + b + d 和 Y = X - c +
d是有着本质不同的。第一种方式要求打破原先X的构造,而重新的组装其实是有成本的,特别是在X本身非常复杂的情况下。典型的,如果X是经过测试的功能,
重新组装后原先由测试保障的概念边界被打破。
我们可以从Y = X + dX抽象出扰动模型 dX = - c + d
主从分解模式自然的导出逆元概念。
如果没有逆元,我们必然需要分解。但是如果发掘了背景这一概念,在逆元运算下,对背景不是分解让其成为可见的部分,而是采用追加的,增删的方法对背景结构
进行修正,则我们有可能在没有完整背景知识的情况下,独立的理解局部变化的结构。即背景是透明的,知识成为局部的。在Witrix平台中,BizFlow
+ DaoWebAction + StdPage
才构成完整的程序模型,BizFlow其实是对标准模型的差异描述,但是它可以被单独的理解。如果我们从接触程序开始就接受BizFlow,
就可能完全不需要了解数据库访问和前台界面渲染的知识。我们并不是通过在DaoWebAction中设定各种可预见的调用形式,而是在BizFlow中通
过类似AOP的操作方式直接对标准模型进行修正。这种修正中一个很重要的部分就是删除标准模型中缺省提供的功能。
WebMVC之前世今生 http://canonical.javaeye.com/blog/163196
Witrix架构分析 http://canonical.javaeye.com/blog/126467
变化的部分构成独立于原始模型的新的模型,它的结构关系是完备的,可以独立的理解。在原始模型崩溃的情况下,它仍然可能保持有效性。
从物理学的角度看,我们所观测到的一切物理现象,都是某种物理作用的结果,也就是物质结构相对于背景状况的一种偏离。我们只可能观测到变化的部分,因此我们对世界的认识其实只是世界的扰动模型而已,世界的本体不属于科学研究的范畴。
软件技术的发展是一个结构化不断加深的过程,我们逐渐拥有了越来越丰富的结构识别, 表达和处理手段。在这一方向上,
组件技术最终取得了巨大的商业成功。但是区分同时也就意味着隔阂。面向对象技术最基础的概念在于 O = C(O),
对象的复合(Composition)仍然是对象. 然而在越来越复杂的软件生态环境中,这一图景实现的可能性被大大的压缩.
面对层层封装造成的形态各异的表达方式, 不同来源, 不同目标, 不同运行环境下的信息的交互变得越来越困难。我们逐渐丧失了概念上的简洁性,
也丧失了数学世界中的那种穿透一切的统一的力量. 所谓SOA(Serivce Oriented
Architecture)技术试图通过补充更多的环境信息,放弃状态关联,暴露元知识等方式来突破现有的困境。 http://canonical.javaeye.com/blog/33803
这其中一项关键的技术抉择是基于文本格式进行信息表达。
在过去10年中Web技术取得了空前的成功,它造就了互联网这一世界上最大的分布式集成应用。SOA从某种程度上说是对这一事实在技术层面上的反思。基于
文本传递的web技术所表现出来的开放性,可理解性和可观测性,与封闭的,难以直接解读的,必须拥有大量相关知识才能正确操作的二进制结构相比,这本身就
是革命性的创新。不需要特殊的工具就可以非常轻易的察看到网页程序的输入输出,所有交互过程都处在完全透明的检查下,各种不曾事先规划的文本处理手段都可
以参与到web创建的过程中。随着速度,存储不再是我们考虑的首要问题,文本化作为一种技术趋势逐渐变得明确起来。但是任何一种技术思想的成功或失败都不
可能是因为某种单一的原因所造成的,因此有必要对文本化的技术价值做更加细致的剖析。
1.
文本化是一定程度上的去结构化,但是通过这种方式我们获得了对程序结构的更强的控制力。无论我们所关心的文本片断层层嵌套在大段文本的哪个部分,我们都可
以通过text.indexOf(subStr)来实现定位,通过text.replace(oldStr,newStr)实现变换。而在组件系统中,我
们只有通过某种预定的遍历方式逐步递归才有可能访问到组件内部的组成对象。如果某个组件不按照规范实现或者规范中缺少明确的设定,则这一信息关联链条将会
断裂。在一些组件系统中,甚至没有规定一种统一的遍历方式,则我们根本无法定义出通用的大范围的结构定位/变换手段。即使是在设计精良的组件框架中,受限
制于组件的封装性要求,我们也不可能访问到全部的信息。当我们以组件设计者意料之外的方式使用这些组件的时候,就会遇到重重障碍。即使所需的只是微小的局
部调整,因为无法触及到需要修改的部分,我们也可能被迫放弃整个组件。
2.
文本是普适的信道。各种操作系统,各种程序语言所具有的最基本的能力就是文本处理能力,对于文本格式的约定是最容易达成共识的。虽然对于机器而言,理解基
于地址定位的二进制数字可能更加直接,但是所有的应用程序都是由人来负责编制,调试,部署,维护的,如果人可以不借助特殊的工具就可以明确了解到系统内发
生的过程,则系统的构建和维护成本就会大幅下降。
3.
文本表述形式上的冗余性增强了系统的概念稳定性和局部可理解性。在二进制格式中,经常出现的是根据相对地址定位,这要求我们完整理解整个二进制结构,才能
够逐步定位到局部数据块。同时,二进制格式中也经常使用一些外部的信息,例如某个位置处的数据为整型,占用四个字节等。这样的信息可能只能在文档说明里查
到,而在数据体中没有任何的体现,这限制了独立的理解可能性。与此相反,文本格式经常是自说明式的,例如width:3.5px,
这提高了系统的可理解性,特别是局部可理解性。即使我们对系统只具有很少的知识,一般也能根据数据周围的相关信息进行局部操作。一般很容易就能够定位到特
殊的局部数据区,安全的跳过众多未知的或者不被关心的结构. 一个程序新手也可以在很短时间内靠着连蒙带猜,
实现xml格式的word文件中的书签替换功能,而要搞清楚word的二进制格式,并独立编制出正确的替换功能,显然就不是一两周的工作量可以解决的了.
这其中, 可理解性的差异是存在着数量级上的鸿沟的.
4. xml这种半结构化的文本格式规范的兴起, 以通用的方式为文本描述引入了基本的形式约束, 实现了结构表达的均一性.
C语言中的宏(Macro)本质上就是一种文本替换技术,它的威力在于没有预定义的语义, 因此可以超越其他语法成分, 破除现有语法无法跨越的限制.
但是它的危险性在于缺乏与其能力相适应的形式约束, 难以控制. 而在xml格式规范下, 不同语义,
不同抽象层面的节点可以共存在同一个形式体系中, 可以用通用的方式进行定位,校验, 转换等. Witrix平台在动态xml方面发展了一系列技术,
为文本处理引入了更多应用相关的规则, 增强了文本描述的抽象能力和表达能力.
5. 文本作为描述(description)而不是执行指令(execution). C语言的源代码与机器码基本上是一一对应的,
即源代码本身所表达的就是指令的执行过程. 而在Web应用领域, HTML语言仅仅是声明界面上需要展现什么,
但是如何去实现是由通用的解析引擎负责,它并不是我们关注的重点. 描述需要结合诠释(解释)才能够产生实际的运行效果,
才能对现实的物理世界产生影响.这在某种程度上实际上是延迟了执行过程. 一种描述可以对应多种诠释,
例如同样的元数据在前台可以用来生成界面,在后台可以用于生成数据库, 进行数据有效性校验等. 在特定的应用领域中,执行引擎可以是通用的,
可以独立于描述本身不断演化, 因此一种领域特定的描述,它所承载的内容并不是固化的, 而是可以随着执行引擎的升级不断增强的. 例如,
在Witrix平台中, FlowDSL本身所做出的流程描述是稳定的,
但是随着流程引擎不断改进,不断引入新的功能,所有使用DSL的已实现的应用都同步得到升级. http://canonical.javaeye.com/blog/275015
6. Text = Process(Text) 这个不动点在Unix系统中得到了充分的应用: 多个小程序通过管道(Pipe)组合在一起,
可以完成相当复杂的功能. 一个更加丰富的处理模型是 XML = Process(XML). 文本描述很自然的支持多趟处理,
它使得我们可以充分利用全局知识(后续的处理过程可以使用前次处理过程收集的全局信息), 可以同时支持多个抽象层面(例如DSL的不断动态展开).
Witrix平台中的编译期运行技术实际上就对应于如下简单规则: 编译期运行产生文本输出, 对输出文本再次进行编译. 通过这一递归模式,
可以简单的实现动态解析与静态描述之间的平衡. 模板(Template)技术是具有关键性作用的文本生成技术.
out.write("<div>");out.write(value);out.write("</div>");这种
API拼接方式显然不如<div>${value}</div>这种模板生成方式直观且易于使用.
在Witrix平台的tpl模板语言中, xml的规范性使得在多趟编译过程中我们一直可以维持某种形式约束.
7. 不是所有的情况下都应该使用文本. 普元EOS中所鼓吹的XML总线之类的技术是我所极力反对的. http://canonical.javaeye.com/blog/33794
代码生成(Code Generation)本身是一个非常宏大的概念。从某种意义上说,当我们明确了计算的意义之后,所做的一切都只是一系列代码生成的过程,最终的目标是生成某种可执行的机器码。对web程序员来说,代码生成是最熟悉不过的了,每天我们所做的工作就是JSP=>Servlet=>HTML。不过,现在多数人脑海中的代码生成,指的一般只是根据配置输出一个或多个程序文本的过程,最常见的是根据数据库模型生成增删改查相关代码。这种技术其实很少在小型以上的项目中起到积极的作用.因为一般的生成工具都没有实现追加功能,无法适应模型的增量修改。此外一般生成的代码相比于手工书写的代码要更加冗长,需要被直接理解的代码总量不降反升.为图一时之快,所要付出的是长期的维护成本。
在应用开发中,有些领域是非常适合于使用代码生成技术的。例如根据领域模型生成ORM(对象-关系映射)描述,或者根据接口描述生成远程调用代理/存根 (Proxy/Stub)等。因为它们实际上只是对同一信息的不同技术形式或者不同技术层面的同义反复而已。这种生成最理想的方式是动态进行,可以随时保持模型的有效性。RoR(RubyOnRails)框架中ActiveRecord技术便是一个成功的范例,它甚至提供了动态生成的DAO函数,减少了一系列的包装调用过程。
代码生成更加深刻的应用是完成高层模型向低层模型的转化,这一过程往往是非平凡(non-trivial)的。在Witrix平台中通过代码生成来支持领域抽象,可以用非常低的成本跨越结构障碍,将自定义的领域模型嵌入到现有的技术体系中。这其中我们的主要工作是解决了生成代码与手工书写代码之间的有效隔离及动态融合问题,确保代码生成可以反复的以增量的方式进行,同时支持最细粒度处对生成的代码进行定制调整。
举一个简单的例子,假设现在需要开发一个三步审批的流程,每一步的操作人可以录入意见,可以选择通过或者回退,可以选择下一步操作的具体操作人,系统自动记录操作时间,每个操作人可以查看自己的操作历史等。虽然在现有技术体系中实现这一功能需要不少代码,但是在业务层面上描述这一功能并不需要很多文字,实际需要提供的信息量很小。显然,建立领域模型是比较适合的做法,可以定义一种DSL(Domain Specific Language)来描述这一模型。
<flow_cp:SeqFlow>
<step id="draft" userField="draferId" dateField="draftTime" waitStatus="drafted" />
<step id="check" userField="checkerId" dateField="checkTime" opinionField="checkOpinion"
waitStatus="sent" />
<step id="approve" userField="approverId" dateField="approveTime"
opinionField="approveOpinion" waitStatus="checked" passStatus="approved" />
</flow_cp:SeqFlow>
以上功能涉及到多个操作场景,实现的时候需要补充大量具体信息,其中很大一部分信息来自于背景知识,例如显示样式,界面布局,前后台通信方式等。以上模型可以进一步抽象为如下标签
<flow_cp:StepFlow3/>
在不同应用中复用以上流程逻辑的时候可能需要局部修正,例如
<flow_cp:StepFlow3>
<step id="check" userField="checker" />
</flow_cp:StepFlow3>
更加复杂的情形是DSL本身提供的抽象无法满足全部需求,而需要在局部补充更多模型之外的信息,例如物品接收单审批通过后自动导入库存等。
在Witrix中,代码生成不是直接产生最终的输出,而是在编译期生成基础模型,它与补充描述通过extends算子进行融合运算之后产生最终输出, 这种融合可以实现基础功能的新增,更改或者删除。典型的调用形式为
<biz-flow>
<extends>
<flow_cp:StepFlow3>
<step id="check" userField="checker" />
</flow_cp:StepFlow3>
</extends>
![](http://www.blogjava.net/Images/dot.gif)
<action id="pass_approve">
.
</action>
</biz-flow>
这里的操作过程可以看作是BizFlow extends SeqFlow<FlowConfig extends StepFlow3Config>,与泛型技术非常类似,只是需要更强的局部结构控制能力。
按照级列理论 http://canonical.javaeye.com/blog/33824 ,我们可以定义一个DSL的级列,整个抽象过程为
Context0 + DSL1 + EXT0 = DSL0
Context1 + DSL2 + EXT1 = DSL1
在目前一些通用语言中,也有一些所谓内嵌DSL的方案,可以提供比较简洁的业务描述。但是仅仅建立DSL描述是不充分的,从级列理论的观点看,我们必须提供一种DSL的补充手段,能够在细节处补充DSL模型之外的信息,实现两者的自然融合。同时我们应该可以在不同的抽象层面上独立的进行操作,例如在 DSL1和DSL2的层面上都可以通过类似继承的操作实现局部调整,这同时也包括在不同的抽象层面上都能对模型进行合法性校验。
软件系统的构建之所以与建筑工程不同,无法达到建筑工程的精确性和可控性,其中一个很重要的原因在于建筑的产物是一个静态的结构,建筑的过程主要是采用各种预制件填充某个规划好的建筑空间,而软件是一种动态运行的产品,它的各个组成部分之间的关系不是可以静态描述的,而是存在着复杂的交互关系,而且软件在运行的过程中还需要根据需求的变化进行动态的调整,这种动态性使得软件开发中很难抽象出固定的预制件,很难像建筑工程那样实现标准件的组装。现在所谓构件技术的构件插拔图景其实是具有误导性的。
但是从另外一方面说,软件内在的动态性使得它可以具备更强的适应能力,如果所编制的软件把握住了业务机制的核心内容,则在运行过程中只需要进行少量调整就可以应对大量类似情况。100栋类似的建筑需要花费100倍的建造费用,而100个近似的软件需求的满足可能只需要花费2至3倍的开发费用。现代软件企业在研发过程中都在不断的追求自身产品的平台化,其目的正在于以不断提高的适应性来应对不断变化的客户需求。我们所面对的要求不仅仅是精确把握需求,而是要深刻把握需求背后所对应的业务机制。
AOP(Apsect Oriented Programming)概念的正式出现也有一些时日了,但是它在程序构造过程中似乎仍未找到合适的切入点,一般系统的设计实现很少将AOP作为必要的技术元素。AOP作为一种普适的技术思想,它所代表的是程序结构空间中的定位和组装技术。 http://canonical.javaeye.com/blog/34941 AOP使我们可以通过非侵入性的方式动态修改“任意”已经构建好的程序,而不需要事前有大量的设计准备。原则上说,这种技术思想是可以在任何程序语言基础上进行表达的,并不是只有java, C#这样的面向对象语言才允许AOP操作. Witrix平台中所应用的部分技术与AOP有些类似,只是大量的结构调整表现为xml生成和xml变换,在具体的使用方式上也有一些微妙的差异。 http://canonical.javaeye.com/blog/126467
相对于通用程序语言,xml语言其实是AOP技术的一个更加合适的形式载体。
1. xml格式特殊的规范性确保了在最细的逻辑粒度上,程序结构也是可识别的,可操纵的(在这一点上非常类似于函数式语言)。而所有的命令式语言(imperative language)中,函数内部的结构都是很难采用统一方式进行描述和定位的。
<ns1:loop>
<rpt:Row/>
</ns1:loop>
2. xml节点的坐标可以采用xpath或者css选择符等通用方式进行描述,而一般程序结构无法达到xml格式这样的均一性,其中的坐标定位方式要复杂得多。
3. xml节点上可以增加任意属性,不同的属性可以属于不同的命名空间(namespace),这些属性可以辅助AOP的定位机制。而一般程序语言中如果没有Annotation机制, 则定位只能依赖于函数名和类名(函数参数只有类型没有名称),而类名和函数名随时可能因为业务变化而调整(不是专为定位而存在), 由此构建的切点描述符是不稳定的。
<ui:PageTable pager="${pager}" cache:timeout="1000" />
4. xml节点的增删改查显然要比字节码生成技术要简单和直观得多。
AOP技术难以找到应用的一个重要原因在于很多人机械式的将它定位为一种横切技术,认为它的价值完全在于某个确定的切面可以插入到多个不同的切点,实现系统的横向分解。而在实际应用中,业务层面上很少具有可抽象的固定的共同性,我们所迫切需要的一般是对已有程序结构进行动态扩展的一种能力。横切是AOP的一种特殊的应用,但不是它的全部。相对于继承(inheritance)等依赖于概念诠释的结构扩展机制,AOP所代表正是对程序结构空间进行任意操纵的一种能力。AOP可以为基础结构增加功能,改变原有功能实现,也可以取消原有功能实现,它不需要把所有的扩展逻辑按照树形结构进行组织,不要求在基础结构中为扩展编写特殊的代码。这种自由的结构扩展能力在Witrix平台中被发展为“实现业务代码与平台基础架构之间的动态融合”。
在Witrix平台的实际应用中,AOP的切点匹配能力并不是十分重要。一般情况下我们主要通过整体结构规划来确保控制点意义明确且相对集中,因此不需要额外通过切点匹配进行业务功能的再组织,不需要再次从杂乱的程序逻辑中重新发现特殊的控制点。例如在Witrix平台的Jsplet框架中所有后台事件响应都通过objectName和objectEvent参数触发,在触发后台事件响应函数之前都会调用bizflow文件中的beforeAction段。
在bizflow文件中,aop操作是明确指定到具体函数的,使用模糊匹配在一般情况下只会使问题变得不必要的复杂化。例如扩展actQuery函数
<action id="aop-Query-default">
<source>
通过自定义标签抽象出多个Action之间的共用代码
<app:DoWorkA/>
</source>
</action>
在Witrix平台中结构组装主要是通过自定义标签库和extends算子来实现,它们都依赖于xml格式的规范性。
1. 通过在custom目录下实现同名的自定义标签,即可覆盖Witrix平台所提供的缺省标签实现,这里所依赖的并不是复杂的匹配过程,而是自然直观的映射过程。 http://canonical.javaeye.com/blog/196826
2. 所有的xml配置文件支持extends操作,它允许定制两个具有业务含义的xml节点之间的结构融合规则。例如<biz-flow extends="docflow">。
实际使用中, AOP技术的一个应用难点在于状态空间的管理问题。一般interceptor中所能访问的变量局限为this指针所携带的成员变量,以及函数调用时传入的调用参数。interceptor很难在状态空间中创建新的变量,也很难读取在其他地方所产生的状态变量。例如对于如下扩展 A(arg1); B(arg2); C(arg3); =〉 Ax(arg1); B(arg2); Cx(arg3); 因为原有的调用序列中没有传递额外的参数,因此A和C的扩展函数之间很难实现共享内部变量x。在TPL模板语言中,tpl本身是无状态的,状态变量通过外部的$thisContext对象统一管理。通过这种行为与状态的分离,结合灵活的变量作用域控制机制,可以以比较简单的方式实现扩展函数之间的信息共享。
说到分解,很多人心中的意象大概只有正交分解。正交分解无疑是最重要的一种分析方法,它也是所谓“分而治之”思想最常见的实现策略。但是正交分解一般潜在的假定是分解后的子部分是大致均衡的,它们是相对具有独立价值的,可以彼此脱离独立发展。这是分解后实现系统解耦的重要原因。 http://canonical.javaeye.com/blog/33885 但是物理学中另一种重要的分析学思想是微扰论(Perturbation). 针对一个复杂的物理现象,首先建立一个全局的规范的模型,然后考虑各种微扰条件对原有模型的影响。在小扰动情况下,模型的变化部分往往可以被线性化,被局域化,因而问题得到简化。微扰分析得到的解依赖于全局模型的解而存在,因而这是一种主从关系的分解方式。但是如果主体模型是我们已经熟知的物理现象,则我们关注的重点可以全部放在扰动解上,认为所有特定的物理规律都体现在扰动解中。如果微扰分析得到的物理元素足够丰富,则微扰模型本身可以成为独立的研究对象,在其中我们同样可以发现某种普适的结构规律。
Witrix平台中系统化的应用主从分解模式,通过类似AOP的技术实现了业务模型与平台技术的自然结合。 http://canonical.javaeye.com/blog/126467 最近我们的一个产品的新版本即将在全国范围内部署,如何有效的控制众多相近的二次开发版本,同时确保主版本的快速升级,是在架构层面必须解决的问题。 http://canonical.javaeye.com/blog/73265 在Witrix平台中,各部署版本并不是直接修改主版本源代码得到,而是将差异化代码放在单独的目录中进行管理,由系统运行平台负责将差异化定制代码与主版本代码进行动态融合,实现部署版本的客户化。在这一过程中,系统模型本身支持逆元结构至关重要,否则某些多余的元素无法通过差异性描述去除,则将出现局部模型失效的情况。
Witrix平台定义了特殊的_custom目录,它的内部目录结构与defaultroot目录相同,系统平台优先使用该目录下文件所提供的功能实现。同时定义了系统参数global.app_id和global.default_app_id,它们分别用来区分当前程序版本以及程序主版本代码。例如当global.app_id=beijing,global.default_app_id=main的时候,系统中装载ui.xml这个标签库时经历如下过程,
1. 装载平台内置的标签库,文件路径为 /_tpl/ui.xml.
2. 根据global.default_app_id设置,装载/_custom/main/_tpl/ui.xml, 其中定义的标签实现将覆盖平台缺省提供的标签实现。对于那些不需要特殊定制的标签,继续使用平台提供的缺省实现。
3. 根据global.app_id设置,装载/_custom/beijing/_tpl/ui.xml, 其中定义的标签实现将覆盖产品主版本的标签实现。
基础平台中对于代码动态融合定义了精细的融合策略,将通过编译技术检查扩展标签的接口与缺省实现的接口相兼容,由此确保代码扩展后不会破坏主版本中的已有调用代码。
在基础平台的实现中,很多实现代码都是类似
<df:WhenAllowFinishWf>
<df:FinishWfButton />
</df:WhenAllowFinishWf>
这样的类似废话的标签调用。但是通过这些标签的标记,我们确立了系统的逻辑结构,标定了系统中可以被安全替换的逻辑片断。
在与一些年岁较大的C程序员接触的过程中,可以比较明显的感受到C的思维方式与面向对象思想的不同。C的世界很清澈,先做A, 再做B, 我们所期待发生的计算过程与源代码的结构是直接一一对照的。这意味着程序将要执行的计算过程在编写代码的时刻就已经确定下来。面向对象首先需要确定的是类,对象等中间元素,而并不是最终的计算过程。对象可以之间可以产生很复杂的结构关系,透过这种中间逻辑结构我们来理解最终要发生的计算过程。在事件驱动的应用场景下,面向对象是一种更加有效的描述,
o.someFunc() o.onEventA();
sub1.someFunc(); ==> sub1.onEventA();
sub2.someFunc(); sub2.onEventB();
如果把对象看作是函数+状态的集合,则对象组装的关系实际上是函数集合之间的一种组装关系。当具体的事件发生的时候,将触发对象上确定的响应函数,此时在各个层面上所实际发生的计算才能被确定下来。
所谓WebMVC即Model2模型是目前Web开发领域的主流模型,Struts/Struts2框架是其典型实现。在概念层面上,这种程序组织模型是怎样建立起来的?与其他Web开发模型(如面向对象模型)具有怎样的联系? 它未来可能的发展方向在哪里? 结合Witrix开发平台的具体实践,基于级列设计理论我们可以看到一条概念发展的脉络。 http://canonical.javaeye.com/blog/33824
![](/images/blogjava_net/canonical/web_app_structure.jpg)
1. 外部视角:原始的servlet规范提供了一个简单的面向IO的程序响应模型。一次前台访问由一个特定的servlet负责响应,它从request中读取输入流,在全局session中保持临时状态,向response中写入输出流。在此基础上,JSP提供的模板概念翻转了程序和输出文本之间的相对地位,简化了文本输出过程。至此,这种整体的程序模型基本上只是规范化了外部系统访问Web服务器的响应模型,并没有对后台程序的具体实现制定明确的约束条件。因此在最粗野的后台实现中,读取参数,业务处理,生成页面等处理步骤是纠缠在一起的,很难理解,也很难重用。每一个后台页面都是一个不可分析的整体。
<%
String paramA = request.getParameter("paramA");
ResultSet rsA = ![](http://www.blogjava.net/Images/dot.gif)
%>
result = <%=rsA.getString(0) %>
String paramB = request.getParamter("paramB");
ResultSet rsB = ![](http://www.blogjava.net/Images/dot.gif)
<%
rsB.close();
rsA.close();
conn.close();
%>
2. 自发分离:在复杂的程序实践中,我们会自发的对业务处理代码和界面代码进行一定程度的分离。因为我们可以直观的感受到这两种代码的稳定性并不匹配。例如不同业务处理过程产生的结果都可以用一个html表格来展现,而同一个业务处理过程产生的结果页面可能经常发生变化。一般我们倾向于将业务代码写在页面上方,而界面代码写在页面下方,并使用一些原始的分解机制,例如include指令。这种分离是随意的,缺乏形式边界的。例如我们无法表达被包含的页面需要哪些参数,也难以避免全局变量名冲突。需要注意的是,分层的一般意义在于各个层面可以独立发展,它的隐含假定是各层面之间的交互是规范化的,只使用确定的数据结构,按照确定的方式进行交互。例如业务层和界面层通过标准的List/Map等数据结构交互,而不是使用具有无限多种样式的特殊的数据结构。(在弱类型语言环境中,实体对象的结构和Map是等价的).
<%
List header = ![](http://www.blogjava.net/Images/dot.gif)
List dataList = ![](http://www.blogjava.net/Images/dot.gif)
%>
<%@ include file="/show_table.jsp" %>
3. 规范分离:JSP所提供的useBean和tag机制,即所谓的Model1模型,是对程序结构分离的一种规范化。业务代码封装在java类中,一般业务函数与web环境无关,即不使用request和response对象, 允许单元测试。tag机制可以看作是对include指令的增强,是一种代码重用机制。tld描述明确了调用tag时的约束关系。调用tag时需要就地指定调用参数,而include页面所依赖的参数可能是在此前任意地方指定的,是与功能实现分离的。此外tag所使用的参数名是局部对象上的属性名,从而避免了对全局变量的依赖。很遗憾的是,jsp tag所封装的仍然是原始的IO模型,对程序结构缺乏精细的定义,在概念层面上只是对文本片段的再加工,难以支撑复杂的控件结构。早期jsp tag无法利用jsp模板本身来构造,无法构成一个层层递进的概念抽象机制,更是让这种孱弱的重用模型雪上加霜。在其位却无能谋其政,这直接造成了整个j2ee前台界面抽象层的概念缺失,以致很多人认为一种前台模板重用机制是无用的。在Witrix平台中所定义的tpl模板语言,充分利用了xml的结构特点,结合编译期变换技术,成为Witrix平台中进行结构抽象的基本手段。实际上,xml能够有效表达的语义比一般人所想象的要多得多。
<jsp:useBean id="myBiz" class=" " />
<% List dataList = myBiz.process(paramA) %>
<ui:Table data="<%= dataList %>" />
4. 框架分离:在Model1模型中,页面中存在着大量的粘结性代码,它们负责解析前台参数,进行类型转换和数据校验,定位特定的业务处理类,设置返回结果,控制页面跳转等。一种自然的想法是定义一个全局的程序框架,它根据集中的配置文件完成所有的粘结性操作。这也就是所谓面向action的WebMVC模型。这一模型实现了服务器端业务层和界面层在实现上的分离,但是对于外部访问者而言,它所暴露的仍然是原始的自动机模型:整个网站是一个庞大的自动机,每次访问都触发一个action,在action中可能更改自动机的状态(作为全局状态容器的session对象或者数据库)。struts作为面向action框架的先驱,它也很自然的成为了先烈。struts中所引入的FormBean, 链接管理等概念已经在实践中被证明是无益的。一些新兴的框架开始回归到通用的Map结构,直接指定跳转页面,或者利用CoC(Convention Over Configuration)缺省映射.
public class RegisterAction extends Action {
public ActionForward perform (ActionMapping mapping,
ActionForm form,
HttpServletRequest req,
HttpServletResponse res)
{
RegisterForm rf = (RegisterForm) form;
![](http://www.blogjava.net/Images/dot.gif)
return mapping.findForward("success");
}
5. 横向延展:分层之后必然导向各个层面的独立发展,我们的视野自然也会扩大到单个页面之外,看到一个层面上更多元素之间的相互作用.在面向对象语言大行其道的今天,继承(inheritance)无疑是多数人首先想到的程序结构组织手段.后台action可以很自然的利用java语言自身的继承机制,配置文件中也可以定义类似的extends或者parent属性.但是对于前台页面一般却很少有适用的抽象手段,于是便有人致力于前台页面的对象语言化:首先将前台页面采用某种对象语言表达,然后再利用对象语言内置的结构抽象机制.放弃界面的可描述性,将其转化为某种活动对象,在我看来是一种错误的方向.而JSF(JavaServerFace)规范却似乎想在这个方向上越走越远.JSF早期设计中存在的一个严重问题是延续了面向对象语言中的状态与行为绑定的组织方式.这造成每次访问后台页面都要重建整个Component Tree, 无法实现页面结构的有效缓存.而Witrix平台中的tpl模板语言编译出的结构是无状态的,可以在多个用户之间重用.
6. 相关聚合:对象化首先意味着相关性的局域化,它并不等价于对象语言化. 当面对一个大的集合的时候,最自然的管理手段便是分组聚合:紧密相关的元素被分配到同一分组,相关性被局域化到组内.例如,针对某个业务对象的增删改查操作可以看作属于同一分组. struts中的一个最佳实践是使用DispatchAction, 它根据一个额外的参数将调用请求映射到Action对象的子函数上.例如/book.do?dispatchMethod=add. 从外部看来,这种访问方式已经超越了原始的servlet响应模型,看起来颇有一些面向对象的样子,但也仅仅局限于样子而已.DispatchAction在struts框架中无疑只是一种权宜之计,它与form, navigation等都是不协调的,而且多个子函数之间并不共享任何状态变量(即不发生内部的相互作用),并不是真正对象化的组织方式.按照结构主义的观点,整体大于部分之和.当一组函数聚集在一起的时候,它们所催生的一个概念便是整体的表征:this指针.Witrix平台中的Jsplet框架是一个面向对象的Web框架,其中同属于一个对象的多个Action响应函数之间可以共享局部的状态变量(thisObj),而不仅仅是通过全局的session对象来发生无差别的全局关联. http://canonical.javaeye.com/blog/33873 需要注意的是,thisObj不仅仅聚集了后台的业务操作,它同时定义了前后台之间的一个标准状态共享机制,实现了前后台之间的聚合.而前台的add.jsp, view.jsp等页面也因此通过thisObj产生了状态关联,构成页面分组.为了更加明确的支持前台页面分组的概念,Witrix平台提供了其他一些辅助关联手段.例如标准页面中的按钮操作都集中在std.js中的stdPage对象上,因此只需要一条语句stdPage.mixin(DocflowOps);即可为docflow定制多个页面上的众多相关按钮操作.此外Witrix平台中定义了标准的url构建手段,它确保在多个页面间跳转的时候,所有以$字符为前缀的参数将被自动携带.从概念上说这是一种类似于cookie,但却更加灵活,更加面向应用的状态保持机制.
class DaoWebAction extends WebContext{
IEntityDao entityDao;
String metaName;
public Object actQuery(){
![](http://www.blogjava.net/Images/dot.gif)
thisObj.put("pager",pager);
return success();
}
public Object actExport(){
Pager pager = (Pager)thisObj.get("pager");
![](http://www.blogjava.net/Images/dot.gif)
return success();
}
}
7. 描述分离:当明确定义了Action所聚集而成的对象结构之后,我们再次回到问题的原点:如何简化程序基元(对象)的构建?继承始终是一种可行的手段,但是它要求信息的组织结构是递进式的,而很多时候我们实际希望的组织方式只是简单的加和。通过明确定义的meta(元数据),从对象中分离出部分描述信息,在实践中被证明是一种有效的手段。同样的后台事件响应对象(ActionObject),同样的前台界面显示代码(PageGroup),配合不同的Meta,可以产生完全不同的行为结果, 表达不同的业务需求。 http://canonical.javaeye.com/blog/114066 从概念上说,这可以看作是一种模板化过程或者是一种复杂的策略模式 ProductWebObject = DaoWebObject<ProductMeta>。当然限于技术实现的原因,在一般框架实现中,meta并不是通过泛型技术引入到Web对象中的。目前常见的开发实践中,经常可以看见类似BaseAction<T>, BaseManager<T>的基类,它们多半仅仅是为了自动实现类型检查。如果结合Annotation技术,则可以超越类型填充,部分达到Meta组合的效果。使用meta的另外一个副作用在于,meta提供了各个层面之间新的信息传递手段,它可以维系多个层面之间的共变(covariant)。例如在使用meta的情况下,后台代码调用requestVars(dsMeta.getUpdatableFields())得到提交参数,前台页面调用forEach dsMeta.getViewableFields()来生成界面. 则新增一个字段的时候,只需要在meta中修改一处,前后台即可实现同步更新,自动维持前后台概念的一致性。有趣的是,前后台在分离之后它们之间的关联变得更加丰富。
8. 切面分离: Meta一般用于引入外部的描述信息,很少直接改变对象的行为结构。AOP(Aspect Oriented Programming)概念的出现为程序结构的组织提供了新的技术手段。AOP可以看作是程序结构空间中定位技术和组装技术的结合,它比继承机制和模板机制更加灵活,也更加强大。 http://canonical.javaeye.com/blog/34941 Witrix平台中通过类似AOP的BizFlow技术实现对DaoWebAction和前台界面的行为扩展,它可以在不扩展DaoWebAction类的情况下,增加/修正/减少web事件响应函数,增加/修正/减少前台界面展现元素。当前台发送的$bizId参数不同的时候,应用到WebObject上的行为切片也不同,从而可以自然的支持同一业务对象具有多个不同应用场景的情况(例如审核和拟制)。在BizFlow中定义了明确的实体化过程,前台提交的集合操作将被分解为针对单个实体的操作。例如前台提交objectEvent=Remove&id=1&id=2,将会调用两次<action id="Remove-default">操作。注意到AOP定位技术首先要求的就是良好的坐标定义, 实体化明确定义了实体操作边界,为实体相关切点的构造奠定了基础。 http://canonical.javaeye.com/blog/33784
9. 背景消除:在Witrix平台中, (DaoWebAction + StdPageGroup + Meta + BizFlow)构成完整的程序模型,因此一般情况下并不需要继承DaoWebAction类,也不需要增加新的前台页面文件,而只需要在BizFlow文件中对修正部分进行描述即可。在某种程度上DaoWebAction+StdPageGroup所提供的CRUD(CreateReadUpdateDelete)模型成为了默认的背景知识。如果背景信息极少泄漏,则我们可以在较高抽象层次上进行工作,而不再理会原始的构造机制。例如在深度集成hibernate的情况下,很少会有必须使用SQL语句的需求。BizFlow是对实体相关的所有业务操作和所有页面展现的集中描述,在考虑到背景知识的情况下,它定义了一个完整的自给自足的程序模型。当我们的建模视角转移到BizFlow模型上时,可以发展出新的程序构造手段。例如BizFlow之间可以定义类似继承机制的extends算子,可以定义实体状态驱动的有限自动机,可以定义不同实体之间的钩稽关系(实体A发生变化的时候自动更新实体B上的相关属性),也可以定义对Workflow的自然嵌入机制。从表面上看,BizFlow似乎回归到了前后台大杂烩的最初场景(甚至更加严重,它同时描述了多个相关页面和多个相关操作),但是在分分合合的模型建立过程中,大量信息被分解到背景模型中,同时发展了各种高级结构抽象机制, 确保了我们注意力的关注点始终是有限的变化部分。而紧致的描述提高了信息密度,简化了程序构造过程。 http://canonical.javaeye.com/blog/126467
<bizflow extends="docflow"> <!-- 引入docflow模型,包括一系列界面修正和后台操作 -->
<biz id="my">
<tpls>
<tpl id="initTpl">
<script src="my_ops.js" ></script>
<script>
stdPage.mixin(MyOps); // 引入多个页面上相关按钮对应的操作
</script>
</tpl>
</tpls>
</biz>
</bizflow>
自从离开学校就基本上不再使用C++了,最近却又因为项目上的原因重新走入这一迷失的世界, 感觉很是缺乏一些顺手的工具。首先就是做配置管理有点麻烦, 因为缺乏反射机制, 无法直接映射, 所以一般需要手工书写配置设置功能.
我们希望配置类在配置阶段能够支持动态属性名,
GConfig cfg;
cfg.set("bgColor.b",3.0);
cfg.set("lightEnabled",false);
t_float b = cfg.get("bgColor.b");
bool l = cfg.get("lightEnabled");
但是内部使用时支持直接的属性访问,便于编译器检查, 也提高运算速度。
t_float b = cfg.bgColor.b;
bool l = cfg.lightEnabled;
所幸C++的类型系统能够偷偷的去干很多见不得人的勾当,因此便有了下面这个简易机制。
#define S_P(x) do{if(strcmp(name,#x) == 0) { x = value; return; } } while(0)
#define G_P(x) do{if(strcmp(name,#x) == 0) { value = x; return; } } while(0)
class _GConfig{
public:
bool lightEnabled;
t_float minX;
t_float maxX;
t_float minY;
t_float maxY;
_GConfig(){
// initialize all primitive members
memset(this,0,sizeof(_GConfig));
}
};
class GConfig: public _GConfig{
public:
GColor bgColor;
GConfig(){
}
_variant_t get(const char* name){
_variant_t value;
get(name,value);
return value;
}
void get(const char* name,_variant_t& value){
G_P(lightEnabled);
G_P(minX);
G_P(maxX);
G_P(minY);
G_P(maxY);
G_P(bgColor.r);
G_P(bgColor.g);
G_P(bgColor.b);
G_P(bgColor.a);
}
void set(const char* name, _variant_t value){
S_P(lightEnabled);
S_P(minX);
S_P(maxX);
S_P(minY);
S_P(maxY);
S_P(bgColor.r);
S_P(bgColor.g);
S_P(bgColor.b);
S_P(bgColor.a);
}
};
_variant_t是VC++在<comdef.h>中提供的对变体数据类型的封装。使用S_P和G_P这样的宏可以由编译器检查变量名的正确性。
关系数据库模型在理论上主要解决的是消除数据冗余的问题。关系模型的数学基础是所谓的集合论,而集合的基本含义正是一组具有某种原子性的互不相同的元素。面向对象技术是对相关性进行局域化的一种手段(相关的数据和操作聚集到同一对象名义下),在这一局域化过程中,相同的元素被识别出来,成为独立的对象。从某种意义上说,关系模型与对象模型是殊途同归的过程,是从不同侧面对同一事物的反映。关系模型中,我们关注的重点是元素组成的集合,允许的连接关系定义在集合之上。而在对象模型中,我们关注的首先是横向关联的实体,实体之间具有稳定的联系。在概念层面上,从对象模型映射到一种关系存储模型只是一个分组问题。为了断开实体之间的直接联系,关系模型创造了一个id字段,而对象模型并不是需要显式id的。在关系模型中,关联并不是通过某种存在的结构来表达的(一个实体持有另一个实体的指针,拥有直接联系),而是将直接关联问题弱化为某种计算过程,我们必须检查id的值(不是某种直接的存在性),通过某种运算过程才能重新发现数据之间的关联。
通过id(伴随一个匹配计算过程)来进行间接关联对于保证模型的一致性是非常关键的。在ORM中恢复了对象的强关联其实会造成很多潜在的复杂性。例如为了维护对象层面结构的一致性,在更新父子关系的时候,我们需要同时调用 child.setParent(parent); parent.getChildren().remove(child); 当关联结构更加复杂的时候,这里所需要的维护工作是随之增加的。不过,在ORM中,对象的形态是暂时性的。在ORM的一次session的操作过程中,对于对象状态的修改可以是不一致的。例如我们可以只调用child.setParent(parent); 而不需要同时 parent.getChilren().remove(child); 只要我们在此次session操作中,不需要同时用到parent.getChildren(). 这种关联的暂时性对于很多ORM应用来说是必不可少的。
对象模型中可以直接表达的结构关系比关系模型要丰富一些,例如继承关系,many-to-many, one-to-list等。但是所有这些都不是真正本质性的差异。抛弃概念诠释,基类与众多派生类之间的关系基本上可以等价于一组one-to-one关系。而当关联对象本身的重要性凸现出来的时候,当我们无法把它约化为对象上的一些附属特性的时候(例如数组的下标),我们必然要建立相应的关联对象,而这正对应于关系模型中的中间关联表。中间关联表上增加额外的字段是一个自然的扩展过程,而对象模型上做这样的扩充往往表现为形态上的重大的不兼容的变化,例如从getManyToManyEntity() -> getToManyRelation(), 这实际上意味着这里的对象形式是偶然的,简化的。
在原始的关系数据库模型中,所有的表之间的地位是平等的,所有字段之间的地位是平等的(主键和外键在参与数据关联时和其他字段的处理方式一致)。这种概念上的均一性和普遍性往往被认为是理论的优美之处。但是现实世界是复杂的,发展的方向就是逐步识别出不同之处,并找出自然的表达形式将这些不同表达出来。均匀的关系模型是对称性最高的,最简化的模型。在面对物理约束时,它隐含的假设是集合之间很少发生相互作用,单表(表单到数据表之间的映射)和主从表是最广泛的情况。试着想象一下关系模型,在思维中一般我们只能看到两个数据表,当考虑到多个表的时候,因为这些表之间没有明确的可区分性,因此它们的意象是模糊的。只有明确意识到主键,外键,主表,从表,字典表,事实表,纬度表这些不同的概念的时候,当对称性出现破缺的时候,我们思维中的模型才能够丰富化起来。
关系模型理论应用到数据库具体应用中时,并不需要死守关系范式教条,它们只是描述了某种极端化的对唯一性的追求。面对具体应用的时候,理论本身也在不断丰富化。我并不把现实应用中必然需要增加冗余字段看作是关系理论失效的结果。从关系完全分解,到关系完全不分解之间,我们可以建立大量的模型。建立冗余字段的时候,我们存在着大量可能的选择,到底哪一种选择是最优的,理论方面仍然可以给我们以具体的指导。理论在各种不同纯化程度的关系模型中都可以给我们以直观的建议。数据仓库理论中建立的snowflake模式和star模式,强调了针对主题域的允许部分冗余的关系分解。这里实际上是强调了表之间的不同性。不再是所有的表都处于同一地位。Fact Table和Dimension Table之间的区别被识别出来,并被明确处理。在我看来,这是原始关系模型的一种自然发展,它也是关系模型理论的一部分。理论不应该是单一的,而是提供一个模型级列,在不同的复杂性层次上,我们可以根据理论的指导选择具体的实现模型。
关于ORM http://canonical.javaeye.com/blog/111500
关系模型中的所谓关系是在使用时刻才定义的,所有建立关系的方式在某种程度上都是等价的,也是外在的。而在ORM中主键与外键之间的关联被独立出来,成为模型内置的部分。这在很多时候简化了数据查询的结构构造过程。
在ORM中主键因为缓存的存在而显出与其他字段的区别。ORM的使用使得数据存储的分解策略得到扩充。并不是所有的表的更新频度都是一致的,而且表中的数据量大小也不同。字典表一般较小,而且很少更新,可以安全的复制。在整个数据存储框架中,ORM作为独立的技术元素参与数据存储过程,通过主键提供缓存服务,产生了新的数据分布模型,提供了新的性能优化契机。
我平时Kill Time的主要方式是阅读各类学术书籍。但是学习本身只是了解前人的发现,间或锻炼一下自己的思维能力,对自己的工作和生活并没有什么直接的助益。学习本身无论如何深入,多半也只是采用前人的话语复现前人思考的历程。在我们通过独立的思考获得直观的体验之前,在我们将所学到的知识应用到书本之外的场景之前,我们所学习的知识只是考试的工具,只是可供欣赏的对象,却不能成为我们思考中活跃的因素,无法参与我们思维的进程。别人只能给你思想的引子,并不能给你真正的思想。只有自己找到了所学知识的超出书本的非显然的应用,只有独立建立了不同概念之间未曾阐明的联系,我们才能真正获得对于真理的体悟。无论我们自己的发现是如何的微不足道,无论它是否只是重复发现了劣质的轮子,它唯一的意义只在于它是我们独立思考的结果,是我们自己的创造。即使它是卑微的,是重复的,它对我们的理解的作用仍然是任何外在的教诲都无法比拟的。
现代社会中创造所需的成本已经被空前降低了。想想百年前的天才们,他们缺少信息来源,只能一页页翻查文献,反复誊写文稿来保存知识,大量的时间被花费在了与思考完全无关的事情上。同时,他们所处的时代存在着更多的不确知性,他们的思考所能够凭依的事实更少,做出错误判断的可能性与今天相比也更大。即使Bill Gates这样的挣钱能手在1981年也能放出"640k ought to be enough for anybody"的厥词,显示出我们在预测未来的时候是何等的乏力。当我们今天站在人类文明的巅峰,拥有前人无法想象的工具,并不断制造着从未经历过的实践的时候,我们理应拥有超越历史上任何天才的,更加宽广的眼界。
现代社会中的创造看似简单了,但从另一个方面看,却又是大大的复杂化了。前人的成就成为了难以逾越的丰碑,而为了进入科学殿堂,我们需要的准备工作也变得异常的繁复。这里有科学内在的规律,但也有人为制造的障碍,而这其中最主要的是数学障碍。只要想一想,针对软件工程,经济运行,企业管理,每个参与其中的实践者都可以提出一些自己的意见,但是如果涉及到数学,为什么大多数人只有三缄其口了?现代抽象数学取得了辉煌的成就,但是它也可能毁掉了更多学生创造的激情。宏伟精深的大厦让人敬畏,却无法激发我们任何直观的共鸣。甚至Arnold这样的数学大师也坦承读不懂当代数学家们的著述:因为他们从不说“彼嘉洗了手”,而只是写道:存在一个t1<0,使得t1在自然的映射t1->彼嘉(t1)之下的像属于脏手组成的集合,并且还存在一个t2,t1<t2<=0,使得t2在上面提到的映射之下的像属于前一句中定义的集合的补集。当Bourbaki学派致力于在课本上消灭所有图示的时候,理性达到了非理性的彼岸。
有时我在想为什么现在的程序员似乎对于程序的理解能力降低了。排除教学水平的降低和个人努力的不足之外,是否是因为现在需要学习的内容过多,以至于丧失了自我思考的勇气?在C的时代,每个程序员对于程序的理解都是直接的,原始的,对程序结构的把握都是充满自信的。当新的概念不断涌现的时候,人们总是说,Object不过是..., Component不过是..., AOP不过是..., ORM不过是..., IoC不过是.... 这体现了人们试图把新的概念融入自己原有知识体系的一种努力。虽然仔细考究起来,这里的理解很多时候都是似是而非的,未必掌握了新技术真正创新的思想方向,但是这里的思考总是独立进行的,总是对我们的理解和工作有所助益的。而新一代的程序员生活在Object, Pattern等概念已经无需饶舌来证明自己的时代,他们是否在思想中独自评估过所有概念的意义,是否建立了概念和实现之间直观的联系,是否在统一的思维世界中为所有的概念找到了合适的坐标?这一切,我不得而知。
推荐:Arnold 论数学教育 http://www.ieee.org.cn/dispbbs.asp?boardID=64&ID=25892
我在各种场合一直都在强调结构问题是独立的,在程序语言之外存在着独立的,可研究的,富有成效的结构问题。 http://canonical.javaeye.com/blog/147424 在这个方向上更进一步,我们注意到所有的代码并不是天然出现的,而是由人所编制的,因此代码世界内部并不构成封闭的,自足的某个世界。代码中的结构问题并不是由代码本身完全解决的,即在代码之外仍然存在着技术上可研究的结构问题。
我们在编制代码的同时也在编制着大量的说明文档。这些文档描述了代码片断之间的相互关系,描述了代码未来的扩展方向,描述了代码之间的可能的交互方式,同时也描述了针对现有代码实现的很多具体约束。例如我们在文档中约定某个量要在10和20之间,但在代码中却不一定显式进行了判断。针对代码结构的很多具体约束条件和相关性描述可能只在文档中体现,只在程序员的头脑中存在,而并不一定忠实的在代码结构中得到表达。
我在设计领域基本持有一种物理实在论,即某种技术相关的约束应该在技术世界中通过技术手段得到表达。只是这里的技术手段却不一定指在应用中加上特定的代码实现,虽然我们在代码实现中更直接的表达设计要求无疑是需要提倡的。为了在程序中有效的维护结构相关性,我们并不一定需要抽象出所有可能重用的代码,并不一定需要确保某一概念在程序中只有精确的唯一的表达。程序中难以直接精确表达的弱关联,仍然可以通过开发/设计工具等技术手段得到有效的维护。我们需要保证的是代码世界中知识的自恰性,而自恰性并不等于唯一性。 http://canonical.javaeye.com/blog/33788
在Witrix中我们采用一种物理模型驱动的开发方式, http://canonical.javaeye.com/blog/29412 由pdm模型出发,自动生成hibernate的hbm文件,java实体类,元数据meta文件,前台Action注册文件等。生成的配置文件通过syncWithModel标记与模型保持着稳定的关联。所有配置文件都支持手工修改,开发工具识别syncWithModel标记,当pdm模型发生变化的时候,工具自动将变化信息同步到各个配置文件中。注意到这里并不是采用一个统一的元数据模型的方式,各个配置文件所表达的信息在一定程度上是重复的,也可能是不一致的。例如后台数据库允许保存100个字节,但是前台meta中我们可能配置只允许录入20个字节。根据不同应用场景的需要,我们可以在各个层面对每个配置文件进行独立的调节. 当然,在一般情况下并不存在这种需要。整个开发过程中,信息表达的自恰性并不是在应用程序代码中得到定义的,而是因为开发工具的存在而在技术上得到保证的。放松模型之间的唯一匹配要求,我们可以得到更加丰富,更加灵活的软件结构。实际上我认为RoR(RubyOnRails)采用直接映射的ActiveRecord的方式虽然有效保证了系统变动中知识的一致性,但是如果不允许在各个层面上都能够自然的定义某种偏离,它在复杂应用中的价值就要打上大大的折扣。
设计考虑的是最终需要什么,最终我们要提供的调用接口是什么,我们所直接需要的某个有价值的,直接存在的,直接可以接触的结构是什么,而不是它所依据的原理是什么,不是它的具体构造过程或者构造方法是什么。比如说我们在程序中完全不需要内置Map这一结构,因为它可以通过列表等结构组合构建出来,但是很显然,Map这一概念的直接存在对我们来说是方便的,是经济的,是有效的。我们在思考的时候并不需要考虑它是采用List实现还是采用Set实现,或者任何它本身的构造结构。这一概念在我们的思想中成为某种原子化的东西。
那么,我们到底需要构建哪些概念,才能够最方便的基于这些概念应对万千应用系统的开发呢。 这是我们需要在结构空间作出探索的。 这里的思维方向不是把系统推向某种纯粹化,某种极致的简单化,而是让它更加物理化,揭示出更多的层次,更加关切在物理约束情况下如何实现灵活性的最大化。一种概念在物理上如果被证明能够在很多场景下成为不变的基元,则它便是有价值的,是可以进行物理诠释的。
很多人习惯于接受语言存在的现实,接受设计后的结果,但是作为程序语言设计者,他们又是如何工作的?他们是否真的是从纯粹的数学关系推演得到所有的语法特征,还是他们预先已经在心中设定了应该出现的语法特征,然后去寻找它的数学表达,只是借此清除思维中潜在存在的矛盾性呢?
语言中存在的所有特征是经过全局考虑的,是清除了所有概念的矛盾冲突的。但是在现实中,我们偶然构造的结构却可能是局限于当下的信息构造的,因此它们相会的时候,可能会出现不协调,可能会出现结构障碍。例如同样是关闭操作,有些人命名为close, 另一些人命名为destroy. 可能一个具有额外参数,另外一个没有。这里可能需要一种adaptor接口的封装,也可能使用ruby那种method-missing的动态判断。对于更加错综复杂的结构问题,其解决方案就不是那么显然的了,但这并不意味着我们无办法可想。究竟设计何种结构边界才能最小化结构融合时所要付出的代价呢?结构被识别并表征出来以后,是否允许它在一定范围内有所变形?在变形中我们需要保持的拓扑不变量是什么?结构动态调整的时候,我们是否需要定义调整的物理代价,是否能够定义某种动力学?
我所阐述的只是在计算机理论中从数学视角向物理视角的转换,它不是必然给你提供某种超越当下的能力,而是提供一种不同的眼光看待所有的一切。视角变换后,我们发现了一些新的命题,而在原先的视角下在我们的话语体系中原本是无法表达这些命题的。串行程序假设了只有1颗CPU, 而函数式语言假设了可以有无限多个CPU, 你不觉得1至无穷之间缺点什么吗。我们可以创造一些东西把1至无穷之间的空白补齐,概念空间是连续的。
没有人否认抽象的意义,但是抽象是否就是抽象到无穷大,这是个可以明确定义的问题,也是数学领域正在解决的问题。在我们的思考中没有明确定义何处是边界,
没有明确的限制,这便是导向无穷的一种思维方式,它和现实中是否真的允许消耗无限多的资源,创建无限多的对象无关。当我们认为自己明白了终极的意义,明白
了一种推向无穷的抽象,这并不是理解了世界的全部,我们仍然要明白如何解决一些更加小范围,但是却又普遍发生的事情。
例如现在我的系统中只需要10个相互依赖的线程,如果我们定死了10这个数字,显然我们可以发展一种这个领域特有的高效的一些算法结构。而抽象到通用语言
中的时候,显然我们只能假设线程数是任意大,或者是充分大的,而无法充分利用10这一领域信息,因此在这个意义上我说通用语言不是有效的。
说到10这个确定的数字,不过是一种极端化的比喻。我常说概念是连续的,并不是非此即彼的,因此并不是从一种普遍情况到一种最特殊的情况之间不再有其他情
况了。在这中间环节,存在着非常深刻的复杂的物理事实。但是这些事实却又是某种有限性向我们揭示出来的。(请不要把这里的有限性理解为算术中的10以内)
现在来一个理论推演吧:
1. 任何系统都在一定约束下运行,即它们需要符合某些约束条件
2. 通用语言描述了某些结构,但是这些结构是充分通用的,能够应用到尽可能广泛的领域的
3. 线程数=10这个约束过份特殊,显然通用语言是不会考虑这个约束。实际上目前在通用语言设计中,无限资源假定基本都是默认的。
4. 我们承认某些现实的约束通用语言是不会考虑的
5. 在最特殊的,明显不会考虑的约束以及非常通用,一般通用语言必然考虑的约束之间,是否存在着更多的,非平凡的结构呢
6. 假如10年以内我们所有的硬件都只能支持10个内核,在我的产品研发中假定10个线程有问题吗。难道我在开发的时候就不再需要抽象了吗。我在其他方面仍然是需要建立抽象的。
7. n个抽象约束+1个具体约束,以及 n个无限约束+1个有限约束 仍然是有效的抽象形式。原谅我在这里又使用了有效一词,它真的很难理解吗。
8. 不是在我们的思维中存在着具体的或者有限的物理量,就意味着这种思维是不抽象的.
函数式语言或者任何一种其他我们已知的与Turing机等价的语言,它们在某种数学的含义上说是"没有差别的"。但是在我们的实际使用过程中,显然我们是
能够感受到它们之间的结构差异的。否则那些不断发明新的函数式语言的人的大脑都进水了吗?在具体使用中,总有人偏好这个语言,有人偏好那个语言,
总是某种情况下应用某个语言会方便一些,另一些麻烦一些。难道在所有函数式语言中开发类似ErLang解决的那些程序结构都是一样方便的吗?
有些人的论调是无论啥都逃不出函数式语言的思想。但是假如现在限定你必须使用java语言来开发商业应用,难道你罢工吗?如果你不使用函数式语言,你所做
的工作就不是程序工作了?你所解决的难道不是程序结构问题了吗?现在就是有一个结构问题要解决, 它是和语言无关的. 语言提供了就可以直接用,
语言没有提供我们可以写代码构造. 难道除了语言直接体现的结构之外, 程序本身就无法构造任何具有通用价值的结构了吗?
我在说到函数式语言的时候,基本的态度只是说不要太沉迷于一种特殊的抽象方式,应该多看看别的视角。在不同的情况下存在着做事情的不同的最优方式。思维中不要只允许永远,而容不下现在。
非此即彼,天下唯我,是我们思维中经常陷入的一个误区。现在计算机领域所谓的理论主要是基于数学视角的,没有考虑物理世界因为我们观察的有限性,因为资源
的有限性所造成的种种约束,此外数学中目前也没有考虑到物理世界真实的各种复杂性的存在。在我们想到一种计算机理论的时候,图像过于简单化,这种简单化被
认为是优美的全部。其实我们应该思维更加开放一些。
数学上的有效性与物理中的有效性是不同的,例如对于密码学问题,如果通过穷举法破解密码成功时,经过这些密码加密的数据已经过了有效期限,此时我们在数学上定义穷举法不是一种有效的破解方法。但是物理层面上我们说只要一种方法比另一种方法能够更快的解决问题,我们就说第一种方法比第二种方法有效,而无论密码被破解的时候该密码是否已经过了有效期限。
我所表述的论题并不是说特定的领域结构无法在某个特定的通用语言中有效实现。我想很多人对我的话语都有些误解。
如果我们认为一种通用语言是比较稳定的,则它一般选择只内置一些通用的不带有领域特定含义的概念. 而缺乏领域知识,或者说因为通用语言故意的摒弃领域依赖, 它在处理领域相关的问题的时候并不是有效的.这种有效性不是数学含义上的,而是可以进行物理度量的.
现在ErLang对通信领域具有良好的支持,你可以说它对于通信领域的结构是有效的。但是显然在ErLang中编写界面就不如面向对象语言得心应手。在ErLang中实现界面结构的时候,它对于界面结构的表述就不是那么符合我们直观的,对我们的实现过程来说就不是那么经济的。因此在界面结构的实现上,目前我们可以说ErLang相对于面向对象语言而言就是不那么有效的。也许你会说ErLang做XX发展之后怎见得就更差。但是如果允许引入未来这一具有无限可能性的因子,我们基本上无法针对现实的情况作出判断。例如我们目前并无法证明广义相对论相对于牛顿力学是更加精确的,如果允许在太阳星系中增加越来越多的隐蔽的摄动星体的话。按照库恩的科学革命论,每一个科学时代都具有着自己的科学范式,它总是具有着充分的自我辩护能力。范式的更新意味着格式塔的崩溃。回顾历史,哥白尼刚提出日心说的时候,并不是在计算精度,计算简洁性上真的远胜托勒密的地心说,只是日心说的哲学隐喻撼动了人心。
我说实际上现在的通用语言也是无法有效承载Domain Specific Structure的,这并不是意指在通用语言中无法针对特定应用作出特定扩展来支持特定的结构,而是说Domain Specific Structure是任意多的,作为通用语言它不应该把越来越多的结构内置在语言中(这不是很多人对ruby的希冀吗),这么做对它来说首先是不经济的。同时某些特殊的结构在一定的场景下是有用的,但是把它抽象出来扩展到通用领域的时候,会出现有效性的丧失。例如现在我的系统中只需要10个相互依赖的线程,如果我们定死了10这个数字,显然我们可以发展一种这个领域特有的高效的一些算法结构。而抽象到通用语言中的时候,显然我们只能假设线程数是任意大,或者是充分大的,而无法充分利用10这一领域信息,因此在这个意义上我说通用语言不是有效的。
传统上数学使用的一种逼近范式是:当n趋于无穷大的时候,偏差趋于无穷小。现在物理学对数学的一种常见要求却是:当n限定在有限数量范围的时候(例如10以内),我们如何才能尽量减少偏差。这要求对小样本数学进行深入的研究,它所具有的物理内涵也是不同的。
在物理的视角下,我们所关心的不是世界在终极的意义上能否分解为函数的复合,不是要导向一种宗教式的顶礼膜拜,而是强调要尊重自己所直接感受到的,充分利用我们因为在这个世界上存在而获得的直观意象,发掘自己的直觉,这样我们才能在无限复杂的世界上借助有限的信息做出选择。
因为在认识上物质和意识的两分,传统上哲学便分为唯物和唯心两大流派。唯物主义因为非哲学的缘故在国内占据主导地位。但在哲学上却从未平息过思想的纷争。胡塞尔的现象学试图重新回归形而上学,摒弃物质和意识的二元论,将它们统一到现象这个一元概念之中。我个人的思想基本是唯物主义的(毕竟少年时受过太多官方的教育),但是对我们目前所掌握的所有知识却持有温和的怀疑。
当一个非哲学家说到“客观规律是客观存在的”的时候,这是以他者的态度,从上帝的第三只眼看世界。但是我们此时此刻却存在于此一世界,因而不可避免的要卷入这滚滚红尘当中。为什么我们在应用分析和综合方法之后,能够通过螺旋式上升的路径逼近终极的真理?这就如同说一个迭代总会收敛,而且必然收敛在正确的结果之上。我们所认识到的一切为什么不能是为了应用方便而不自觉采用的机会主义的产物?所有可能的哲学都是我们在此生有限的存在中所无法证实或证伪的。当我开始质疑为什么我们不能是离(我们在有限时空范围内认为的)本质愈近,离真实愈远的时候,这不过是用一种无意义置换了另一种无意义,但是一个人的观点却因为他的情感的注入而对他显出不同。我们如何才能有勇气,有力量,有方法在有限的时空中创造出变化?
人类千年的智慧传承到我们这些普通的接受者手中的时候,它们都不可避免的陈腐了,庸俗了。当我们在说到这些真理的时候,我们的喉舌已经脱离和思想的联系。这些永真的话语在唇齿间跳动的时候甚至不能在我们的思想中激起微小的涟漪,它们不过是鹦鹉学舌。
道是无情却有情。有人说我们对规律的揭示是分析,实验和直觉的产物。可是直觉是什么?这里的逻辑类似于“如果A则B,否则其他一切情况则X"。所谓的直觉是一切非直觉的补集,实际上涵盖了我们所有丰富的情感历程和生存体验。理性却需要非理性来拯救。我们的行为总是受到情感的驱使。例如我在blog上写下一篇关于函数式语言的文章,一些技术爱好者纷纷发表议论,这是否是理智推演的结果?来的时候一腔热情,去的时候是否真的带走了什么事实?这样的事情每时每刻都在我们的世界中发生。我现在并不喜欢"XX之道"的提法,其实所论与道无关,只是利用了传统上对于道的敬畏,绑架了我们真挚的情感,却没有带来任何真实的体验。多情使得我们成为被忽悠的对象。
活着,用海德格尔的话说,一个字“烦”,用萨特的话说,两个字“恶心”,用普罗大众的话说,三个字“不想死”。但如何去活?中国人将个人的命运化入了家国的绵延,按李泽厚的话说,实现了心理学和伦理学的统一,在生的途中实现对情感的构建和塑造,这并不是单纯的个人生命体验,也不是宗教中神秘性的情感迷狂或心灵净化。
我在前面的文章中列举了大量物理学相关的例子来试图说明采用物理视角的必要性,但是可能因为物理事实大家不熟悉,结果直接被无视了. 在本文中我想有必要举一个软件领域的例子。只是在实际思考的过程中,我主要还是基于物理概念进行推理.
首先我所谓“现在的通用语言”,它并不意指“现在至未来所有通用语言之合集”,而是指“目前正在被使用的某一种通用语言”,这种差别便体现了我所强调的不同的价值观和不同的视角。不是一种覆盖一切的全称判断,而是在特定物理约束下的物理实体。
现在无论我们设计什么大型系统,一般总是要优先考虑微内核设计。但是很显然,如果我们的编程控制能力极强(强大到不现实的地步),我们可以把所有的代码实现为一个大的整体。一个整体的好处是勿用质疑的,否则Linux Torvalds就不会有信心和Tanenbaum PK。但即使是Linux, 随着系统越来越庞大,在内核中也补充了很多模块管理策略。我并不把这种情况看作是一种现在技术能力不到位所造成的结果,而是把它看作是在现实的物理约束下所促成的一种必然的选择。
按照类似的逻辑,我认为在通用语言层面不应该导入越来越多的特征,实际上也不可能把所有可能的结构方式都内置在语言中(这种不可能不是数学意义上的不可能)。这会破坏一种语言的纯洁性,使得它极难维护和发展。为了扩大通用语言的有效应用范围,一种显然的方式是在语言中定义一些支持结构再次抽象的机制,通过可插拔的方式实现与domain相关的知识的融合。ruby这样的语言提供了大量的元编程机制, Witrix平台中tpl模板语言也发展了一系列编译期结构构造技术, 但是显然它们都不能说是结构抽象技术的终极形态. 目前我对所有通用语言所提供的结构抽象和结构组装能力都是不满意的,因此在Witrix中发展了一些领域特定的结构融合手段.例如根据"继承"关系的结构诠释(继承可以看作是两个一维集合之间的覆盖关系), 我们扩展了extends的结构操作方式, 定义了广义的extends算子. 这些特定的结构关系目前在领域特定的BizFlow语言中体现, 它们在通用语言中是难以想象的, 而把它们放置在通用的语言中也是不合适的(这种复杂的结构融合操作如果不能结合领域知识进行直观的理解, 必将导向一种思维的混乱). 这就是我所谓"现在的通用语言无法有效承载Domain Specific Structure"的含义. 这种说法其实类似于"集合论是无法包容所有数学结构的". 我们在集合论中只研究最普遍的关系,而特定的结构在特定的学科中研究.
关于ErLang的例子, 我的原意是用来说明结构问题是独立的,它是和具体语言无关的.即基于消息传递发生数据关联的超轻量级进程模型这一结构不是和ErLang语言绑定的. 为此我特意加了一段说明:"这里不是要证明某种语言中无法描述这些结构,而是说结构是客观存在的,它并不是要在基础语言层面得到充分解决的". 即使在语言层面我们并不解决这个结构问题, 它仍然客观存在着,我们仍然可以用其他的技术手段去定义,去解决. 解决了这个结构问题就必然会带给我们价值,而无论我们使用何种实现语言.
"什么原因,什么样的约束条件,导致了现在的通用语言是无法有效承载消息传递发生数据关联的超轻量级进程模型". 这一命题并不是我原文中论点的合理推论.我并不是要说某一种特定的领域结构无法在一种特定的通用语言中得到支持.而是说如果我们认为一种通用语言是比较稳定的,则它一般选择只内置一些通用的不带有领域特定含义的概念. 而缺乏领域知识,或者说因为通用语言故意的摒弃领域依赖, 它在处理领域相关的问题的时候并不是有效的.这种有效性不是数学含义上的,而是可以进行物理度量的. 现在也有很多人认为ErLang并不是真正的通用语言,它是针对通信领域进行了特定结构调整的, 是内置了领域特定结构的. 而目前在ErLang上建立GUI的努力也并不算是成功.
在前文中我举了一个例子试图说明:"在限定的物理约束下,我们的选择范围会大大缩小". "比如说我现在有无穷多种方式从北京跑到上海,但是如果限定只允许用1升汽油,那么我们的选择就近乎于0". 这里并不是要说明加上物理约束之后,我们便没有任何选择了.而是说物理约束对无穷多的可能方式起了限定选择的作用, 它最终造成我们在具体的物理场景下可能只有非常有限的选择. 例如现在允许用100升汽油, 有多少种运输方式可以满足我们的要求? 如果允许1000升呢? 但是如果不考虑所有物理约束, 我们是否能够证明说: 飞机和拖拉机的运输能力是完全一致的, 因为它们都能从北京开到上海.
我的观点是结构问题是独立存在的,它具有自身的价值, 研究它也需要建立特定的价值观. 一个结构可以体现为语言上的某种语法特征, 也可以通过框架等实现, 或者表现为某种设计模式,某种编程技巧. 我们在思考结构问题的时候并不是从特定语言的机制出发的, 当语言不直接支持的时候我们可以发展特定的实现技术支持它. 在未来的日子里某个结构可能被证明具有普适的价值,它会被吸收到某个通用语言中成为所有程序的支撑结构, 但是更多的结构永远都不会进入通用语言, 而是居留在某个特定的领域. 通用语言的发展并不是完全基于抽象的数学分析而进行的, 它可以从更加丰富的物理世界中吸取营养. 当一种结构进入通用语言的时候, 它所带来的绝对不只是一组数量关系,而是同时带来一系列经过实践检验的物理诠释.
我所谓的领域并不是指业务领域, 而是结构领域, 一个可以定义特定结构的物理场景. 一个特定的结构仍然可以支撑着任意多的具体应用. 例如CRUD操作可以作为数据管理模型. BizFlow作为界面和单实体的交互模型.
函数式语言为我们提供了一种具体的技术工具, 但是在现实的开发中, 为了有效的处理结构问题, 显然我们需要多种视角的组合, 而不是把所有可想见的图景都纯化为函数. 我们对世界的体验是多样化的. 这就是我所谓"世界比函数的集合要复杂"的含义.
有一位网友,今年大二,询问我怎样才能成为技术高手. 我并不确定如何定义所谓的技术高手, 但是既然问到我, 便列举一下可能相关的事实.
首先请明确以下事实:
A. 挣钱能力和技术能力相关,但不成正比。
B. 他人的信任与信赖和技术能力无关。
C. 泡妞能力和技术能力可能负相关,请谨慎面对。
没有人知道未来是什么样子. 也没有人能够保证自己永远都是技术大潮中的弄潮儿. 如果你只是担心知识的老化, 担心能否长期的保有一份工作. 那么只需要不失去别人的信任,越来越熟练的做好本职工作就可以了。任何工作中当时用到的知识永远都只是很少的部分,只要别人给你机会重新学习,你的人生经验就会是你最宝贵的财富。
大二的小孩不要每天尽整些没用的东西。认真学好自己的专业课。做软件开发并不需要什么高深的知识,但是接受知识传承,得到全面教育的时机却是一去不回的。
不要怕学得多,更不要自以为学得多。我在学校的时候,横扫图书馆的哥们并不少。
读了书并不意味着懂得了道理。用自己的语言能否讲述学过的内容?能否用个简图勾勒Unix内核的结构?学过一门语言到底它有什么不同,能否勾勒发展的脉络?你学过的东西并不是你的东西. 你既无法消费它,也无法贩卖它. 在书本以外你是否真的意识到这些知识的存在性? 最重要的问题是, 你创造的东西在哪里?
读书开始的时候半知半解是正常的。这就如同张无忌背七伤拳经,总有一天你会懂得,只是需要不时的去回味。数学典籍中经常有这样的说法,本书内容是封闭的,只需要某某领域知识,但是掌握它需要数学成熟性。成熟是需要时间的。
读书不是以数量取胜。一个领域中类似的书只要细读一本,完整的读下来,读的时候多方参照,做简短的笔记。读一些原始的文献,读大师的原著。尽量选用英文的经典教材。懂一点学科的历史,知道什么是文化。了解一些学术界的八卦,吹牛的时候多一些谈资。
学习任何一个领域都需要深入, 需要掌握大量的相关细节, 因为只有这样才能够不再被细节问题所干扰, 而集中精力于真正的思想核心.
拳不离手,曲不离口。连Knuth老兄都在勤奋的敲程序,所以请不要找借口, 先编上十几二十万行代码再说话。编写而不是抄写。
天下没有免费的午餐。不付出相应的成本,无法得到相应的回报。学习没有捷径,只有方法。只是方法正确并不能保证你走到终点,毅力,机缘都是不可或缺的。你是否能够1天10小时以上持续地考虑同一个问题,是否能够保持同样的注意力坚持到每本书的最后一页, 是否一年365天对新鲜事物总是保有一份天真的好奇。
在工作中除了抽象的思想和具体的技术知识之外,还有大量小的trick. 例如调用equals的时候把常量放在前方if(MY_CONST.equals(myVar)).
一般不可能通过书本学习掌握所有这些技巧,只能在编程的实践中观察总结,更多的时候是要你主动思考,重新去发现这些方法。一个人的工作效率很大程度上是受其工作习惯所制约的,你是否在随时随地的改进自己的工作?
怎样才能做技术高手?这个问题我并不知道答案。公司里所需要的也不是技术高手,而是能够解决问题的人。不过如何培养合格的程序员,在公司内部也有两种看法。adun说要给他们指明职业发展的方向,关心他们遇到的困惑。这是P大的浪漫主义情怀。X罗说要给他们可以完成但是不易完成的任务,等待大浪淘沙后的结果。这是T大的现实主义精神。
开源是不可阻挡的历史洪流,我们只能改变自己的思维方式,调整自己的行为目标来适应它。
面对未来的挑战,Alan Kay有一句名言:The best way to predict the future is to invent it。如果你不知道该怎么创造,那就先从捏造开始练习吧----事物之间总是可以建立关联的。
现在讲软件工程的, 所谈论的多半是项目工程,
即如何在有限的时间内配置使用有限的资源在单个项目中达到既定的目标. 传统上, 在这一领域基于预测和计划的瀑布方法曾经占据主流,
但是随着项目的日益复杂化, 各种基于演化(evolution)思想的工程方法在实证中逐渐发展起来. 在时空跨度更大的软件工程领域,
例如延展到软件的不同版本以及多个相似项目的工程中, 演化更是逐渐取得了无可置疑的主导地位. 但是, 从另一个方面说,
目前所有这些软件工程方法所推崇的演化实际上都是非常有限的, 它们通过迭代(iteration)所能够描述的演化过程都只是片断性的,
例如一个项目中的演化, 一个软件产品的演化, 最大的莫过于一整条软件产品线的演化. 所有这些演化过程都面临着一个天然的屏障:
商业公司.在公司内部, 知识或代码可以由开发人员携带到下一个项目, 或者从一个小组传播到另外一个小组, 在新的基础上继续演化的进程.
但是核心的知识或者代码一般只能通过商业交易传达到其他公司, 这是一条非常受限制的途径. 而一个单个公司所开发的软件包, 即使是平台级的产品,
如果只是内部使用, 受限于该公司所从事的业务领域, 其所面临的使用检验也是非常有限的. 而且出于经济上的原因,
单个公司往往无力支撑多个实现同样功能的解决方案, 因而它倾向于消灭软件中的多样性, 这有可能会进一步限制演化的进程.
开源(OpenSource)软件为软件演化创造了新的可能性.商业友好的开源软件可以被不同的公司自由的运用于不同的业务,
因而可以参与到不同的局部演化过程中. 在应用的过程中, 开源软件面临着巨大的重构压力(这往往是超越了应用最广泛的封闭源码软件包的),
有可能保持更快的演化速度. 而通过对开源软件的回馈, 对开源软件的改进可以传播到时空范围跨度巨大的软件开发过程中. 而且基于源码的开放性,
开发人员的知识交流也随之开放起来. 类比于Darwin进化论, 我们可以说开源驱动了整个软件业界的共同进化(co-evolution).
多年前, Eric Raymond在著名的文章"大教堂和市集"中 http://263.aka.org.cn/Docs/c&b.html,
提出了开源的工程价值, 但其所关注的重点仍然只是单个软件如何在开源的模式下演化, 从今天的观点看来, 这篇战斗檄文已经显得有些局促了.
开源所造就的巨大演化空间远远超越了软件工程所能够提供的. 开源软件现在已经在商业开发领域站稳了脚跟,也渐渐超越了单个公司能够控制的范围.
可以说开源软件的发展是无可逆转的, 我们已经不会也不应该再回复到原先的封闭世界中.
每当我在文字中对函数式语言有些不敬之意时,便好像动了某些人的奶酪,以至我的言辞总在被曲解后遭到排斥。我想这就是因为视角差异过大所造成的. 但是谦虚谨慎是传统的美德, 不能容纳他人的观点只会妨碍自己在某些方向的探索。
首先请不要轻易怀疑我的知识水平。当然如果总无法聚集起足够的注意力来理解别人话语中的细节,我也无话可说。
容纳他人的观点就意味着不要总在自己的话语体系中试图找到反例. 一个人总是受限于他的知识范围,因此他也经常在自己的知识范围内篡改曲解别人的意见。我从未说过 "一个具体的问题是现有的通用语言无法描述的". 我说的是"现实开发中所需要处理的结构问题并不是在语言层面得到充分 解决的", " 现在的通用语言也是无法 有效承载Domain Specific Structure的". 请注意我对定语和动词的选择。其实我已经举了大量的例子来进行说明,但可能因为大多数人不是物理背景,对相关的内容不熟悉,所以直接无视了。这也很对,符合物理学的精神。
可能大多数人都知道函数式语言和命令式语言都是和图灵机等价的,因此它具有某种终极能力,怀疑它无异于怀疑我们世界存在的基础。但是请注意,这种等价性是数学性的。它潜在的要求是无限的能量和时间消耗。如果在限定的物理约束下,我们会发现我们的选择范围会大大缩小。所以我说"函数式语言和命令式语言的计算能力相同,但是在具体的情形下它们的描述能力是不同的". 比如说我现在有无穷多种方式从北京跑到上海,但是如果限定只允许用1升汽油,那么我们的选择就近乎于0。飞机和汽车的运输能力是相同的吗。物理学的一个基本精神在于一种物理性的约束是始终存在的。而事实上,我们在实际工作中也总是在各种有限的物理条件下工作。
也许有些人认为这种区分是无关紧要的,我们只关心某种终极的东西。但是物理学中有着太多的例证,说明在有限约束下,整个系统呈现出完全不同的性质。在通信领域我们都知道Shannon定理,它的物理诠释是在有噪声的信道上可以 有效的进行 准确的信息传递。但是这一诠释只能在有限的数学精度(远大于我们实际需求的精度)上成立, 在绝对准确的数学意义上,这是不可能的事情。
你觉得现在的通用语言做起领域相关的东西来很方便吗,这就是我所谓无法有效承载的含义。在这里我也没有否认"未来的牛语言可以轻松搞定目前难题"的可能性。
因为所有的软件设计最终都要落实到某种代码实现上,所以怎么会有什么神秘的软件结构是现有的语言无法描述的呢。但是ErLang中那种高并发,支持错误恢复的程序结构是在其他语言中能够轻松实现的吗。很多人不是在潜意识中认为ErLang的成功是函数式语言排他性的成功吗,不是认为命令式语言无论如何实现不了ErLang的程序结构的吗。很显然,在命令式语言中是无法直接实现ErLang中的程序结构的,否则它就变成了函数式语言,但是所有发生在ErLang世界中的事实都一样可以发生在命令式语言的世界中。ErLang语言的编译器可以是使用命令式语言实现的,在终极的意义上,语言之间能有什么区别呢?
我说"实际上现在的通用语言也是无法有效承载Domain Specific Structure的", 这还有另一层含义。通用语言设计总是要考虑到内置结构的某种通用性,设计时能够凭依的信息较少,因此不可能直接制造某种复杂的领域相关的结构。而目前已知的通用语言中提供的结构抽象的手段也不够强大(实际上我认为任何语言都不会强大到内置所有结构,也无法提供所有的结构抽象手段), 相当于是把领域结构问题推给程序员解决。这就如同C语言把内存管理推给程序员解决一样。现在ruby比较流行不就是因为它能够动态处理很多结构问题吗,但是它现在所作的一切就是足够的了吗。难道二十年之后再来看这个语言,不能够发现它存在着巨大的改进空间吗。我们目前在Witrix中通过tpl模板语言,bizflow extends等机制,结合整体框架设计实现了一些与ruby不同的结构构造方法。这些手段都极大的增强了我们面对领域问题时的信心,也确保了我们的领域知识是技术层面上可积累的。但是即使这样,我对程序发展的现状就是满意的吗?难道不存在更加丰富的结构知识等待我们去发现吗?一般人总是习惯接受已经存在的现实,在有限的职业生涯中把它们当作不变的真理,却没有耐心的去思考如何去改变。
我认为很多结构问题不是需要在语言层面得到解决的,而是应该在独立的结构层(平台,框架)进行解决。这意味着没有必要在语言层面直接内置某种特定的结构,内置某种特定的结构抽象手段。这基本类似于说不要把集合论扩大到包含所有的数学关系,请在别的学科分支中进行研究。需要注意的是,我所谓的领域知识不是特定的业务知识,而是从业务知识中可以分析得到的某种更加通用的普适的结构知识,甚至是可以使用数学进行精确描述的。
现代软件发展的时间还很短,与数学和物理学这样深刻的学科相比,它无疑是相对幼稚的,是待成长的,是更加的不完美的。在程序构建的基本问题上并没有抽象出什么可以实际操作的精确规律。这是所谓Pattern在软件业流行的部分原因:我们希望用这种半形式化的方式捕获某种思考的结果。但是软件真的除了基于抽象数学的全局的全称性的证明之外,不能够在局部进行某种更加复杂,更加严谨的分析吗。
我们说结构问题是独立的,这也意味着它和具体的实现语言具有某种意义上的分离性。通过一种语言书写的结构可以在另一种语言中得到表达。我们可以建立语言中立的技术结构。一种所谓的结构在概念上具有某种确定的形态,我们可以脱离具体的语言来理解它。例如我说"面向对象的继承关系从结构观点上看是两个一维集合之间的覆盖关系". 在java中我们可以直接使用语言提供的继承机制,而在C语言中我们就需要建立某种结构体,手动维持所有的指针关联。而在Witrix平台中,我们从继承的结构诠释出发,定义了更加复杂的extends算子,这就需要利用java语言编制特定的parser来实现了。但是显然,在思考的时候我们所有的思维指向是结构本身,而不是任何通用语言的语法。
在物理学中,通过摄动分析我们可以清楚地意识到:同样一个物理现象对应的数学模型可以是众多的,但是在特定的参数区我们会选择某种特定的数学表述,并确定其中的待定参数。
delta函数是物理学家狄拉克引入的,在Schwatz引入分布概念建立广义函数论之前,物理学家们已经使用这一函数工作了很多年。后来Abraham Robinsen利用数理逻辑方法,建立了非标准分析,通过模型论的方法精确定义了无穷小的概念,从更加直接的角度论证了delta的合理性。但是在物理学家看来,这些数学又有什么区别呢?物理学只是按照物理的诠释进行工作,具体的数学只是它可选的工具而已。
物理的真理并不是蕴含在数学中的,它需要我们独立的探索,从与数学不同的观点进行思考,检验,最终我们才能做出真正的发现。广义相对论可以采用Riemman几何进行描述,但是它的物理诠释却是Einstein提出的. 没有人说Riemann或者Hilbert发现了广义相对论。另外一方面,因为Einstein的工作触发了对于微分几何的更加深入的研究,靠着物理直觉的导引,我们将这一数学分支推进到了难以想象的深度。"数学是无法涵盖物理学的". 这不是说最终物理学无法采用数学语言进行描述,而是说在这一发展过程中,所有思想的推动来源于物理学的经验,来源于我们在这个物质世界上所进行的反复验证。不是在一个封闭的小屋中,整天摆弄各种数学符号,我们就能够发明所有的物理公式所对应的数学。实际上,现在学术界普遍承认,没有物理学的推进,很多数学的进展是不可能发生的。
物理系每天都在演算着Feynman路径积分, 但是所有人都知道这是没有什么严格的数学依据的.目前并无法定义路径积分的收敛性,但是所有人对此避而不谈. 只要形式演算合法,物理预测符合实验, 合理性的证明只是数学家们的事情. 在量子场论中所采用的重整化(Renormalization)方法不过是回避无穷大问题的一种形式手段.我们仍然无法在数学层面对所有的演算都给予合理化解释. 在更多的物理分支中工作,你就会发现物理学家的胆子不是一般的大。也许在未来我们能够发现这些物理过程背后数学机制的精确定义, 但也许最终我们也无法找到合适的定义方式. 但这对物理学家来说, 并不是很大的打击.因为指引我们的是物理直觉,是独立于数学的物质世界的意象。
我所想讨论的不是某种终极意义上的可能性,不是绝对概念之间的冲突,而是在物理现实的约束下,我们如何才能有效工作的问题。我已经反复表述了自己的观点: " 结构是可抽象的,是具有独立意义的。这就是Witrix所提出的面向结构的设计视角。不是强调对象的所谓业务含义,不是强调某种通用语言(例如ruby)的灵活的语法结构。在这之间存在着厚重的具有物理意义的可以进行结构分析的技术层". 也许有人觉得我说的这是废话, 但是当系统化的执行一种思想的时候,就会揭示出未预料到的可能性. 整个Witrix平台简单的说起来就是" 面向结构的级列分析", 但是如何找到合适的技术形式来体现这一思想,却绝对不是一件平凡的事情. "在Witrix中我们实现的代码重用程度和程序整体结构控制能力是超越了目前所有已知的公开技术的。这不是什么哲学,而是我们在残酷的商业竞争中得以生存的资本". http://canonical.javaeye.com/blog/126467
在我看来,计算机领域充斥着纯数学的深沉遐想和从工程实践而来的轻佻常识,还没有注意到物理学所能带来的不同的同样深刻的视角。我常说,好好学习物理是必要的,因为这个世界远比你想象的要复杂的多。
我的观点并不是什么具体的程序结构问题不能用函数式语言处理.我所要表述的是这和函数式语言中能否加入结构解决任意复杂问题无关。为什么所有的问题不能在集合论中解决,为什么要有独立的数学学科。物理学所有的定律都使用数学表述,是否意味着物理学的真理蕴含在数学之中。
我说 实际上现在的通用语言也是无法有效承载Domain Specific Structure的。其实与以下说法是类似的
数学是无法涵盖物理学的,现在的已知的数学工具是无法有效承载尚未得到充分探索的领域的物理的
我说 我所关心的不是语言层面的问题。这类似于说 不要把所有物理问题都推到数学层面去解决。
我们应该研究独立的结构,应该建立单独的价值观和方法论。不要谈及一个技术进展的时候就说某某语言好,不是一说到DSL的优点就要去抱ruby的大腿。此外,我的观点也不是去做业务分析,不是去如何更好的实现业务到基础技术结构的映射。
不是强调对象的所谓业务含义,不是强调某种通用语言(例如ruby)的灵活的语法结构。在这之间存在着厚重的具有物理意义的可以进行结构分析的技术层。
我想说这个结构层面现在并未得到充分的关注,我们对于结构的问题并不是非常清楚,对程序结构的稳定性更是少有经验。我们在Witrix中做了大量的工作,试图做到如下的图景:
永远只写代码片断,而所有的代码片断组合在一起又构成一个可理解的整体
对背景不是分解让其成为可见的部分,而是采用追加的,增删的方法对背景结构进行修正,则我们有可能在没有完整背景知识的情况下,独立的理解局部变化的结构。即背景是透明的,知识成为局部的。
http://canonical.javaeye.com/blog/126467
在Witrix中我们实现的代码重用程度和程序整体结构控制能力是超越了目前所有已知的公开技术的。这不是什么哲学,而是我们在残酷的商业竞争中得以生存的资本。
号外:
不要把具体的技术和一种技术思想混为一谈。一种实现总是包容了太多的思想。思想错了,实现对了。实现死了,思想活着。
关于哲学的问题是很有意思的,因为它是引起最多思想冲突的地方。原本[关于认识的悖论]这篇blog是我在重温维特根斯坦之后反思自己的哲学观念所写下的一些文字。我在具体的推理中是明晰的,但是在哲学思想方面一直是混乱的,所以估计对旁人而言它确实没有什么可读性。不过如果有人感兴趣愿意讨论一下, 我想说明一下我在哲学方面的几个观点:
1. 哲学于我而言是严密的逻辑论证,还是浪漫的情感诉求?
2. 我不认为认识的终极指向所谓的规律, 因为即使我们掌握了所有的规律(包括规律的规律?),仍然解决不了规律为何存在的问题。
3. 当我们自以为对XX的认识越来越清晰,越来越深入的时候,是否已经偏离了XX? 偏执的认识可以激发强烈的情感,聚集思维的资源,引导我们穿越未知。但是新的联系怎样从出人意料的地方生长出来?
4. 维特根斯坦晚年创建了分析哲学的日常语言学派。有人说他变成了星宿派,有人说他发现了新大陆。但是无论如何,自然语言作为人类所创造的思维工具,对人们思维的潜在影响是难以估量的。在人造语言中,借助罗素的类型论我们似乎可以回避问题,但是并无法终极解决逻辑系统的循环依赖。波利亚在数学家中曾经做过一个有趣的调查,询问著名的数学家在思考的时候是否会依赖自然语言,回答各异,但很少有人说严格按照公式进行推理。以我个人而言,思考时更多的是某种似曾经历的意象,而很少是严格的逻辑表述。创造的依据是否是事件之间难以言传的微妙联系,抑或是原始的创造力只是源于错误的巧合?
5. 西方哲学从笛卡尔开始,从本体论转向了认识论。但是是否现在我们已经认清了自己的所在?矛盾的产生是事物内在的真实存在的特点,还是因为认识层面的割裂而人为造成?有多少冲突是因为没有发现协调方式而造成?分析和综合之后我们能够达到什么?螺旋式上升的比喻让我感到有些眩晕。
6. 东方的思维方式不是分析法的,也很难说是归纳法的。这种所谓的整体论难道只是披着神秘外衣的巫术?现在学术界流行的是以西方的思维工具来重新诠释一切观念,我的思想中到底受到各方观点何种影响?难道故老相传的东西都只成了被研究的客体,成了手术台上待解剖的木乃伊?
7.从我们的词汇表中抹去那些词语之后我们将无言以对?例如规律两字。
以上所论的一切都是无价值的,因为其实我只是想问自己一个问题:明天我的灵感在哪里,如何回答。
1. 函数式语言可以合理的看作是泛函分析思想的一种体现,从历史发展的时间上看大概也是具有这种可能性的。在Backus的论文中对函数式语言的一种乐观的情绪甚至扩大到functional algebra can be used to tranform programs and to solve equations whose "unknowns" are programms in much the same way one transform equations in high school algebra。这种信心只能来源于泛函分析和方程论,来自于数学分析学的发展。将函数的函数作为分析对象就构成泛函分析。泛函分析的最核心的思想当然不等价于它所研究的那些无穷维空间,不等价于种种正交基的构造。它的思想核心在于函数的函数具有独立的分析价值,具有脱离数量空间的具体的结构。这也是函数式语言试图推销的一种理念。
2. 最近这些年来一种称为"范畴"(Category)的东西在计算机理论研究中频频出现。范畴论是从同调代数发展而来的一种较新的代数语言,但它显然也不是可以解决任何问题的灵丹妙药。多一种表达方式当然在某种程度上可以增进我们的理解。但是范畴本身只是研究一种基础结构,它本身并没有承载所有的物理事实,基于它不可能对所有的规律一网打尽。不是明白了范畴,就懂了程序。范畴论是一种基础性的语言,有些人致力于基于范畴论来构建数学的其他分支,取代集合论的地位。将计算的本质重新归结于范畴论是无意义的,它不过是对事实的另一种陈述方式。说“函数式语言是基于范畴”是无意义的,因为这和说“所有现代数学都基于集合论”一样。无法发现新的相互作用关系,所有的概念都只是同义反复。不是一拿起数学,就找到了组织。
3. 我对函数式语言并没有什么反对意见。它是非常重要也非常优美的一种技术思想。但是现在一些函数式语言的狂热支持者似乎对函数世界充满了乌托邦式的幻想,一种大一统的世界观让人迷醉,但是它却解决不了现实的问题。所以我说无法认同函数式编程的世界观。作为一种具体的技术工具,问题不在于函数式语言是否体现了计算的本质,而在于它是否向我们提供了称手的兵器。现在我要计算两个小球相互碰撞的问题,我可以操起广义相对论,量子力学啥的开始大干一场,也可以用个牛顿力学小试牛刀,甚至可以只用反射关系摆个等式。但是在绝大多数情况下我们都会说这里面的物理是弹性反射而不是相对论。在理论分析中我们经常使用平面波假设,但只要实际关心的对象不在波包的边缘,没有人会认为平面波不是真实的物理机制。理论物理不是理想物理。在具体的参数设定下,我们只会使用特定的物理学。
对世界的认识并不是非此即彼的。并不是说函数式语言好它就永远都好,要把所有对立面都灭掉。也不是说函数式不好,命令式就必然的好,就必然的能够解决问题。函数式语言的世界观过分单纯而排他,这是我反对的,我同样无法认同面向对象的本体论论调。就像CISC和RISC架构之争一样,最终我们在现实的物理约束下,运行的最好的芯片是两者思想的结合。这是我们面对物理世界的基本态度。
4. 函数式语言中时间是个有趣的概念。命令式语言中因为赋值语句的存在,使得我们可以观测到状态的变化,因此必然要定义时间。而函数式语言是无状态的,可以是无时间概念(对Lazy Caculation的依赖是否体现了深层次上对时间概念的需求?)。有些人认为函数可以看作是相空间中的迁移函数,是与相对论协调的,因而反映了时间的本质等等。相对论主要是解决了物理规律的协变性的问题,在此过程中它使人们认识到了时空之间奇异的对称性。但是广义相对论的表述中时间也是可逆的。真正定义了时间之箭的是热力学第二定律。根据Landauer's principle: 擦除(erase)1比特信息,耗散到环境中的能量至少是k*T*ln2, 或者说熵增至少是k*ln2. 这意味着只要我们对眼前的黑板不停的写了擦,擦了写,就必然无法回到过去。物理世界是复杂的。
5. 如果将状态看作是可以附着在某个对象上的标记,显然状态的存在性便于我们识认概念的唯一性。对象还是那个对象,只是状态标记发生了变化。而如果系统中没有状态,则必然产生了一个新的概念。这在很多情况下是不必要的负担。状态的存在使得系统在局部结构上允许出现非常复杂的变化,函数式编程的拥趸们对此多有诟病。但是从另一个方面上说,状态使得我们可以基于局部信息处理很多问题而不需要把它扩大化为一个全局匹配问题。
6. 函数构成函数似乎是很完备统一的世界。 但是在物理世界中发生的一切却复杂的多。虽然世界可以还原为原子,但是原子构成分子,分子构成宏观物质时,系统的基本性状发生了本质性的变化,并不再是统一的形式。每一个层面上都会产生独立的结构规律。
7. 函数式语言和命令式语言的计算能力相同(可以相差一个任意长度的多项式时间),但是在具体的情形下它们的描述能力是不同的。我所关心的不是语言层面的问题,因为语言本身的能力并不足以解决现实开发中的所有问题。即现实开发中所需要处理的结构问题并不是在语言层面得到充分解决的,这是我们需要做工作的地方。
关于现实中的结构问题,我无意去定义什么万能的描述能力。你可以用微分几何,积分几何,广义变分等等手段去证明圆盘是某种意义下的周长最短的东西,但是这一切对你发明轮子并无本质上的助益。不过可以说说现实中的结构。这里不是要证明某种语言中无法描述这些结构,而是说结构是客观存在的,它并不是要在基础语言层面得到充分解决的。实际上现在的通用语言也是无法有效承载Domain Specific Structure的。
A. ErLang大概是目前世界上应用最为深入的函数式语言了。它确实发挥了函数式语言无显式状态变量的优势。但是它对程序构建本质上的帮助更多的来源于无共享的超轻量级进程模型,相当于定制了一般操作系统所提供的基本服务。微软的一个实验性操作系统项目Singularity, 其中也定义了只通过消息传递发生数据关联的超轻量级进程模型,它使用C#的一个扩展语言,额外增加的功能是消息管道上定义的规格状态机,对消息交互的时空模式进行额外的规约。这里对我们真正有价值的是隔离的单元结构。
B. AOP是程序结构空间中的定位和组装技术。在Witrix中我们规范化了切点处的状态空间,并对AOP进行了偏置处理.这种结构调整大大提高了AOP的可用性,使得它成为Witrix中的核心技术手段之一。
C. 面向对象的继承关系从结构观点上看是两个一维集合之间的覆盖关系。在Witrix中扩展了extends所对应的结构操作,创造了新的结构融合手段。
我习惯于概念层的推演,而且所阐述的东西多数是我们创造过程中的副产品,与业内常见的观念实际上是有着很大差异的。有些人感觉我的文章读不明白是因为没有采用类似的视角,或者还没有独立思考过很多问题。如果只是从业内已经熟知的概念出发试图理解我所写的内容,显然是不可能的事情。所以我常说know something already known.
如果在编制一个新的应用,存在大量代码可能是
myFunc(){
for each x in set
doSomethingValuable(x);
return packedResult;
}
myOtherFunc(packedResult){
for each y in pakedResult
doSomethingOther(y)
}
其实我们真正关心的是循环内部的某个过程,但是我们经常可以观察到它们被某些通用的或者特定的循环(集合遍历)操作所包围着。Witrix的设计方式是强调业务关注点,而把所有的汇总操作尽量抽象完成。比如现在界面上显示一些字段。从抽象的操作上说
for each field in dsMeta.viewableFields
show field.viewer
这一过程在平台代码中实现,它是一个通用的集合操作过程。不同的具体应用只是关心具体字段的展现形式,虽然我们必然需要字段集合,但是它不是我们注意力的重心。
如果考虑到字段在界面上展示有一个布局问题,我们所要修改的是集合内部的结构方式:
某种结构循环方式(dsMeta.字段组成的布局集合)
show field.viewer
抽离出集合,实际上是在最大限度上分离结构问题和内容问题。
结构是可抽象的,是具有独立意义的。这就是Witrix所提出的面向结构的设计视角。不是强调对象的所谓业务含义,不是强调某种通用语言(例如ruby)的灵活的语法结构。在这之间存在着厚重的具有物理意义的可以进行结构分析的技术层。 http://canonical.javaeye.com/blog/60758 http://canonical.javaeye.com/blog/126467
地址(Address)是现代计算机体系架构中的核心概念,它在程序设计语言上的体现就是C语言中的指针(Pointer)。在C语言中,所有的高级技巧都和指针这个概念相关。指针只是一个存放了一个地址的变量,但是C语言中提供了一个方便的间接访问方式,p->x, 它使得拥有指针在概念上就等价于拥有了指针所指的全部内容。在这种诱导下,我们渐渐模糊了地址和地址所存储的内容之间的区别。这也是指针的指针这样的概念总是让初学者迷惑不解的重要原因。
指针是对地址的符号化。它所带来的第一大好处是使得我们摆脱了对绝对地址空间的依赖。如同Newton第一定律所阐述的:物理规律与所发生的惯性坐标系无关。同样,数字空间中发生的的事件与所处的绝对地址也是无关的。在符号化的方向上更进一步,如果我们专注于指针的关联语义,而放弃指针的指针这样的混杂概念,就会得到具有独立价值的引用(Reference)概念.
从表面上看起来,数字空间只是一个无限延展的一维地址空间,每一地址处只能存放一个有限大小的离散数值,似乎它的几何学是贫瘠的。但是因为在软件设计中,一般是不考虑寻址时间的。这意味着在拥有指针的情况下,我们可以“立刻”访问到数字空间的任意遥远的地方。这种超时空的信息传递过程使得我们可以利用“引用”概念轻松的构建一个多维的表示空间。在面向对象的技术背景下,x.y.z这样的形式表示暗示着x,y,z是同时存在的。当z发生变化的时候,通过y.z和x.y的信息传导,x对象本身也发生了某种变化。
随着web技术的流行,独立的状态/地址空间的存在性逐渐成为系统不可回避的假设, "同时性"的物理约束越来越难以维持. 相对论规定了物理现象的定域性, 在数字空间我们一直忽视了它.但有趣的是, 网络上的传输时延却迫使我们重新发现了"引用"形式下必然存在着的物理过程. 引用本身只是标记了某种信息关联, 并不一定意味着同时性约束. 并发编程领域的所谓的Future对象是对传统引用概念的一种有趣扩展.
result = obj.callMethod(args) ==> future = obj.callMethod(args)
future对象可以被自由传递, 只有当实际访问到它的属性的时候, 才会触发时序约束.
判断和循环是程序中最基本的语句结构。而在vonNeumann体系架构下,循环是对集合进行操作所需的基本步骤。一个有趣的事实是,函数式语言所宣称的
生产力的来源很大程度上在于集合操作的便捷性。在数学中我们通过张量分析,泛函分析等可以清楚地意识到集合之间的相互作用是可抽象的,是可以独立理解的,
即我们可以在不涉及具体基元结构的层面上独立的定义并执行集合运算。如何将这种概念独立性在框架层面展开是一个非常深刻的命题。
在基元结构上应用基础操作p(d)这一微观场景一般情况下是容易理解并实现的, 但通常程序中所定义的大量边界是基于集合变量的,
因此很多代码都是封包和解包操作, 在层层嵌套的循环结构深处我们才能发现真正具有业务价值的基元结构.
将集合操作提升到系统层,减少或简化在应用层需要显式编制的循环结构是框架设计层面需要考虑的问题.
一个最基本的方法是尽量定义通用的同构操作, 避免构造中间集合. 例如前后台之间通过json等通用协议交换复杂结构的对象,
避免定义特殊的中间处理过程. 一个与之相配合的重要技术手段是通过类查询语句(描述方式)直接构造特定的集合.
例如prototype.js中提供的$$('div
div.myclass').each(op)这样的处理方式显然要比在循环过程中基于特定条件过滤要方便的多.
而在AOP操作中切点的集合定义方式也是其提供的核心价值之一. 与集合操作相适应的一种代码风格是流式设计(stream),
这正是jQuery试图鼓吹的主要价值(虽然我个人认为它有些走极端). 流式设计的核心结构实际上是 x += dx,
它不需要集合是一次性构造的, 便于支持一种逐步部分修正的概念模型.
为了支持集合的隐式构造, 我们需要以通用的方式定义元素到集合的组装规则.
在Witrix平台的前台js框架中我们定义了由独立的html组件到复合查询条件的拼接规则,
定义了由每个html组件的数据校验函数到整个form的数据校验函数之间的组装规则,
定义了由单个html组件提交参数到整个form提交参数之间的组装规则. 在Witrix平台的标准界面上,
框架本身的编制基于js.query.buildCondition(frmQuery),
js.validate.validateForm(frmUpdate), ajax.addForm(frmUpdate)等少量集合操作进行,
在不同的应用场景下, 我们只需要关心每一个字段如何显示, 提交哪些属性, 而由系统负责将它们自动组装并在后台进行分派. 面向不同的应用,
框架代码不需要做出任何修改, 确保了系统结构的可重用性.
Witrix平台的后台处理模型中定义了实体化过程, DaoWebAction基于CRUD等原子操作定义了批量提交,
数据导入导出等复合的甚至是嵌套的集合操作. 在不同的应用中, 我们通过修改bizflow文件中<action
id="ViewDetail-default">, <action
id="Update-default">等针对单实体的业务规则即可适应不同的业务场景, 而不需要为特定的应用重复编制集合处理过程.
面向集合+通用组装规则是Witrix平台设计中采用的基本设计手法之一,
它使得我们在一般应用中只需要考虑单实体,单字段等基元结构上发生的特定业务, 大大简化了系统构造过程.
但是也需要认识到从个体到集合的扩张(p(d) -> P(D) )是非平凡的, 集合比个体的简单加和要更多,
为此架构中需要保留对集合边界的识别能力, 例如需要允许在数据导入完成之后执行特定的业务规则而不是仅仅针对每一数据行执行业务规则.
由于各个公司的领域,规模,人员配备等差异很大,形形色色的公司中顶着架构师头衔的诸般人等所从事工作的内容以及所承担的责任也是大相径庭。务虚者有之,务实者也有之, 难以一概而论。甚至关于架构一词的具体含义在不同语境下也是很难达成共识的。然而作为架构师,他应该做什么,能够做什么,却是我在自己的职业生涯中需要加以回答的问题。
软件公司中的工作大致分为销售,技术,财务,打杂这几类。架构师所从事的工作大致上属于技术这一摊,应该是一种高度专业化的技术工作。在我看来,一般所谓架构师的工作主要是负责设计规范整个软件项目/产品/产品线的整体结构,他所摆弄的是各种相关的技术元素。虽然作为公司的技术利益的代表者,架构师会在某种程度上参与到公司的商业活动中(在某些巨型公司中,架构师甚至可以通过标准规范对整个产业结构施加影响),但是他更多的是接收商业需求将其转化为技术约束,而很少是商业目标的制定者。业务架构方面的设计更理想的是由业务专家进行,这个工作多半只需要技术的常识,而不需要对于技术本身的深刻洞察。在另一方面,虽然架构师对于技术实现所需的技术/人力等资源需求会提出自己的估算和建议,但是他一般并不具备相应的手段和责任来具体管理整个实现过程。因此在我看来架构师的管理职责并不是很大。当然,有些架构师会更加接近商业和管理而远离技术,将他们称之为"资深架构师"可能更加合适。在某些大型系统的建设过程中,总体设计人员可以只负责收集各个子系统的技术要求,汇总后制定整体技术规范,所起的作用类似于协调人员,在这种情况下倒是对技术要求较低而对管理素质要求较高了。
关于架构的一个有趣的事实是,技术架构本身其实很少存在设计问题。大部分问题只在于业务问题如何分解到既定的技术架构上,一般的技术架构也只是现有技术元素的简单组合而已。所谓的架构设计工作并不是在真正的系统全景下进行,它往往是基于已有经验所作的短暂延伸,是对业内其他类似结构的复制变形。我们所面临的大量问题是选型问题,不是创造性问题,而是选择性问题。架构师最富技巧性的工作不是现在确定什么,做出选择,而是确定现在可以不确定什么,可以将哪些选择延迟。
在一般人看来,架构师对于系统成败必然起着关键性作用,否则他们凭什么属于“活少钱多”的那伙人呢。但真实情况是,商业上的成败很少是由技术架构直接决定的。因为技术开放和快速传播等原因造成了技术的趋同性,在技术层面上,大多数公司很难依靠技术形成差异化优势。竞争优势主要来源于业务理解和与用户的接触性,来自于历史形成的业务格局。而在中国这样一个营销制导的商业世界中,架构师的工作更难说是在构造某种与众不同的东西。只有少数大公司依靠把握标准才形成技术的话语权,大部分人不过是在技术的大潮中随波逐流罢了。“不求有功,但求无过”应该是架构师基本的工作精神。技术失败最常见的原因除了不够专业以外(在中国,“专业”的标准也许是不同的),就是过于自信,试图去创造些新的结构,或者试图全面应用某种不熟悉的技术。架构建设应该是一个逐步改进的过程,不要激进盲动。
国内的架构师多数是从高级程序员发展而来,在工作期间多半是学习掌握外部知识,以掌握知识的细节程度和广度为优先。因为总是在别人搭好的平台上活动,即使是参与过众多大型系统的建设,对于系统整体结构一般也没有提炼出自己的认识观点。而有些大学设置了专业,宣传培养架构师,但是实际上缺乏系统的实践训练,学生所学到的多半是高举高打的套路,在实战中的表现往往更差。掌握技术细节和自主的整体性思考对于架构师而言都是不可或缺的。
虽然创新的技术未必是商业中核心的元素,但是真正的创造性仍然是每一个设计师的希冀。作为一名实践者,我们都在某种程度上期望超越所经历的偶然,达到某种普遍的真理,在外部的物质世界中留下自己的精神烙印。在这种意义下,架构师的工作便不是简单的技术背景或者技术理解可以涵盖的了。我相信,在业务层和基础技术设施之间存在着物理性的厚重的通用技术层,其中存在着大量的结构规律等待我们的探索,这也正是Witrix平台一直努力的方向。
我们总认为认识的最高境界是认识到事物的本质。但是越明晰的认识意味着越明晰的区分,而区分意味着认识到事物的独特性,割裂了它与普遍事实之间的联系。我们是否会说这一本质和那一本质是本质上不同的?本质最根本的意义在于内在的规律,在于内在的协调而不是和普遍事物的对立。我们对本质的认识是如何成为可能的?现代语言哲学发掘的一个基础事实是我们的语言中涉及到抽象事物的部分存在含混性和自我证明的逻辑循环。在抽象的概念上我们很难达到共识, 而这恰恰是通常我们认为所谓本质所寄居的地方. 语言文字是人类所创造的思维的工具,我们对它们的存在早已习以为常。只有研究词源的时候, 我们才能清楚地意识到人们的思维和世界的现象之间的巨大鸿沟. 通过千百年的文化积淀和不断的自我强化, 我们才形成了共同的民族心理结构, 看到同样的文字的时候才能激发我们类似的情感, 才能引起类似的意向,但是具体的过程仍然是不可言说的.
辩证法的两端都能够成为我们认识的对象,因此我们会感到矛盾对立的存在. 但是随着我们认识方向的转移, 很多矛盾可能被弱化,被消解. 在本世纪初, 相对论和量子论无论在理智或者情感上都是如此让人难以接受, 但是新一代的学生接受起来已经自然了很多. 现在我们只需要盲目的遵守规则,而不再需要去寻求自我证明的解释. 在现代物理的框架下, 惯性不是物质本身的属性, 它来自物质之外. 万有引力不是物质之间的额外的相互作用,而是时空扭曲后造成的内蕴约束. 但是在局部情况下, 并不妨碍我们建立一个形式系统使得这个属性内在化。在很多时候只有偏执的认识才能引导我们穿越未知.
中国人的认识论是整体性的,但却不是公理化的.传统上我们说微言大义,总试图从真切的细节处领悟超越的真理. 这是和分析法不同的认识的途径, 但也很难说它是归纳法. 思维中的意象是符号化的, 但是也是具体的,拥有自己的形象,并具有某种潜在的活动性。
读书在传统意义上是走向精英阶层的一条路径,这种功利目的一直深刻在我们的心中。学校也是按照培养高于平均水平之上的人才而设定的。只是在如今这个人人都是大学生的时代,大学生早已不是什么“天之骄子”。缺乏可以用于创造或者交换的技能和资源,知识阶层的相对贬值也在情理之中。依然持有着自己应该高于普通生活水平的错觉,非要在面子上有所交待,负担高于平均水准的车/房,很多时候只是徒增烦恼而已。
读书是获取知识的主要途径,即使在影音世界空前繁荣的今天,它仍然是不可替代的。不知是学生的问题,老师的问题,抑或是整个教育机制的问题,现在接触到的很多人既没有从学校学习到必要的知识,也没有掌握基本的学习方法。很多人津津乐道的是某某很聪明,没见他用功也取得好成绩,某某很能来事,没干多久就挣了大钱之类的传闻。不劳而获可以是一种希望,却很难成为发生在自己身上的现实。我见过的聪明人很多,但是真正能做出一些自己的东西的人,都具备某种专注的能力,都要在某个方向上做出一般人难以达到的持久的努力,所付出的成本往往是不为人所知的。
学习没有捷径,但却是有方法的。有效的阅读需要在一定的时间内完成,在最短的时间内获得整体的阶段性的认识。太厚或者太过艰难的书会耗尽我们的耐心。循序渐进,举一反三,温故知新是平凡的真理。读书一定要做笔记, 否则所获得的印像很快就会因为没有物质凭依而消逝. 笔记不是抄书,而是从自己的视角重新整理并组织,一般最多两三页纸而已。不要纠缠在文字细节上, 而要努力把握其中的一种图景. 物理中非常强调物理图象的重要, 这些图象未必是仿真的, 未必是要把事实世界中的事情复原,它们更多的是符号性的, 所指向的是一种感觉。有些人总是纠缠在什么是OOA, 什么是OOD这样的概念区分上,但是多数时候这些区分都是毫无意义的。我们需要脱离纸面上的图形和文字,想象它们的真实,这绝不是UML那种已经定义了的符号, 而是与世界上更多事物可以发生共鸣的某种形式. 我们所需要的是利剑迎面击来的那一刹那间对它最直接的感受. 思考问题的时候是现在这种感觉和曾经想过或者曾经看过的其他问题的相似, 而不涉及到任何文字上严谨的表述. 很多书上都列出很多条规则, 但是谁能保证这些规则是完备的, 如何才能从唯一的规则实现逐步的分化, 将它们演变出来. 能不能采用自己的语言复述, 能不能找出一个特定的视角重建这些概念的关联. 当把信息抽离到少数符号的时候, 通过空间形象思维我们有可能把握这一点. 空间优于时间, 实际上对于时间的感受我们是通过空间运动来定义的.
现在的世界与百年之前已经是有着本质的区别,知识成为公开市场上兜售甚至免费公开的东西。在前所未有的开放中,人人都具有了创造的可能,所谓的创意早已成为我们惯常的生活,以至于一阶变化已经无法称其为真正的创造了。但另一方面,真正的思想仍然具有本质的稀缺性。原创的思想往往来自于少数人,其他人往往是在某一方向上进行衍生。现在很多人已经习惯了快餐式的文化消费,却不知更应该去阅读大师的原著而不是经过别人蒸馏后的转述。原创的思想在文字中跃动,它所关注的不仅是眼前的事实,而是整个世界和当前事实之间的关联,试图为它寻求到真正存在的价值。
读书是一件有趣的事情,但是将一切都归结于某种感官的快乐,无疑是一种过分浅薄的观念。读书之乐趣未必是真的愉悦的感受。
读书不能使你更加富有,也不一定能带给你安宁, 不一定对你的工作生活有什么帮助. 它只是Kill Time的一种方式而已. 只是相对于电视影像的强制倾销, 游戏竞技的自我沉迷, 它比较适中的维持了一定的自主性和外部性的平衡. 我常说, 读书只是使你明白自己生活在怎样的一个时代, 自己不是一个蒙昧的原始人. 不清楚相对论, 不知道量子力学, 不了解基因技术, 只是很遗憾的在二十一世纪走过.
读书也是读书人在社会上得以自持的一种方式,因为毕竟我们还可以说:拽什么拽,没文化,不稀的理你。
虽然现代物理的标准表述采纳了严密的数学语言,物理学和数学的精神还是有着深刻区别的。数学的目标在于在既定的范围内把有限规则的推理推进到极致。物理学格物以致知,我们所了解到的永远只是这个世界变化的部分。当一种物理机制位于我们的视野之外时,物理学将选择直接无视它。因此物理学对数学是按需使用的,是有限理性的,总要受到所谓物理直观的制约。再完美的物理模型也不过是在不完全信息下建立的一种完备的数学模型。当我们沿着一条推理的链条越走越远的时候,一种本能的怀疑便会油然而生。
Witrix开发平台基于级列设计理论发展了一系列新的设计思想和软件分解机制。并提出了一种新的Web体系架构。 http://canonical.javaeye.com/blog/33824
Witrix架构呈"可"字形态,其中定义了三条主要的分界线:
1. 浏览器和服务器之间通过语义结构明晰的URL形成两分结构。 http://canonical.javaeye.com/blog/99122
2. 系统前台至后台存在一条预制的非侵入的信道. 它维持了一种无害的可扩展结构.
具体的说,系统从前台js到后台处理,对于所有$为前缀的参数是自动传递的,没有识别出的层将自动把这些参数传递下去.系统通过这一信道实现退化过程。在
我以前的文章中曾经指出过, 每一种可退化形式都对应存在一种非侵入性的可扩展设计。 http://canonical.blogdriver.com/canonical/993807.html
3. Witrix内置了对于CRUD模型的支持, 而BizFlow通过类似AOP的方法对CRUD模型进行了扩展。这使得Witrix的模型驱动部分并不是仅仅针对单表或者单实体的维护, 而是可以实现特定的业务逻辑和CRUD逻辑的混杂.
这三条分界线分别规范了基础状态空间,对已有知识的重用以及面向未来的可扩展性。在这种大的宏观结构下,Witrix应用了如下技术手段:
1. 对象化。Witrix中的Jsplet框架以对象的名义提供了对后台的状态和行为空间进行分解的基础手段。 http://canonical.javaeye.com/blog/33873。Witrix
依托于Jsplet对象实现相关性的局域化, 而不需要像一般面向action的框架那样直接访问http session这一全局状态空间.
前台发送的objectName参数同时在系统的不同层面标定了WebAction响应函数, Biz配置, DataSourceMeta元数据,
Hibernate实体等一系列相关概念, 使得它们构成一个统一的整体.
2.
标准化。与REST架构风格类似,DaoWebAction规范化了后台响应事件。DaoWebAction上支持的标准事件有Query,
ViewDetail,Add, Update, Remove等,
它们构成一个完整的CRUD模型。与REST不同的是,DaoWebAction提供了一个空的响应事件BizAction。它相当于是CRUD模型中的
零元操作。在BizFlow模型下,它将被扩展为一个操作子空间,从而实现对于CRUD模型的超越。而在REST模型下所有的扩展操作必须依附于一个已经
具有固定语义的Method上,例如POST. http://canonical.javaeye.com/blog/99122
3. 实体化。在Witrix中充分发掘了ORM技术的能力, 使得单一业务对象上可以聚集到某一范围内的所有相关结构信息.
http://canonical.javaeye.com/blog/111500.
同时在DaoWebAction中通过EntityFilter机制实现了单实体化过程. 这意味着前台应用可以一次性提交多个批量操作,
后台框架负责把它们分解为针对单个实体的一次确定性操作, 在后台实现时只需要考虑单实体的情况即可.
一个简单的例子是前台提交objectEvent=Remove&id=1&id=2&id=3 ,
WebAction层会为每一个id对应的实体调用BizFlow中的Remove-default操作. 实体化是一个非常重要的过程,
它使我们关注的核心成为单实体, 正是因为明确了单实体作为基本的关注点, 我们才可以建立更加复杂的状态机机制, 驱动系统状态变化.
4. 组件化. 前台的tpl模板和后台的WebAction共享一个thisObj指针, 结合自定义标签机制, 资源(js/css等)管理机制等构成可以重用的组件结构.
5. 偏置的AOP. BizFlow通过一种类似于AOP的操作对DaoWebAction提供的CRUD模型进行扩展,
使得模型的能力得到本质性的扩张. 这种AOP操作与通常意义的AOP的区别在于: 缺省行为在默认情况下发生, 除非显式禁止. 通过这种方式,
反转了base和extension之间的主体地位. 此外BizFlow所提供的不仅仅是行为的扩展,它同时提供了对界面的描述.
在前台tpl页面中通过 <ds:BizViewOps/>等无参数的标签调用来定义嵌入坐标. http://canonical.javaeye.com/blog/34941
![](/images/blogjava_net/canonical/jsplet-model.jpg)
与传统的J2EE相比较, Witrix表现出很多变化:
1. 不使用全局的session, 而是使用局域化的thisObj
2. 不定义service层,其功能分解到BizFlow和Handler中,它们都不负责日常的DAO操作。独立的MDA部分负责所有的实体CRUD(Create Read Update Delete)操作。
3. 不定义页面跳转规则,在前台使用拉模式直接表明跳转目标。结合前台stdPage对象在前台控制跳转目标。并可以在BizFlow中配置覆盖的规则,这样就可以针对不同的应用场景定义不同的跳转规则。
4. 不是为每个模块, 每个应用场景编制一组新的页面,而是大多数模块共用少数几个标准页面.
5. 不在与网络无关的service层上定义权限和事务管理。Witrix架构下通过URL明确区分了系统内部和外部,
前台访问后台时调用者的全部意图是以规范化的形式表达在url中的.
因此权限和事务管理作用在WebObject上在概念上也可以认为是约束直接作用在URL上, 这与REST风格是统一的.
当然我们也可以规范service方法的命名等, 但是显然要求一种随意性消失是有代价的,
在URL上我们已经付出了代价,为什么要在service上再付出一次. Witrix中Transaction和Auth的配置更加直观,
因为规范化了WebObject上的事件响应函数,一般我们也不需要进行特殊的配置.
Witrix这种设计更加适合于网络这一两分结构的,更加充分的利用这一架构边界所提供的隔离性.
6. 不在页面中使用实体的字段名,而是大量通过元数据表达程序意图。http://canonical.javaeye.com/blog/114066
一般J2EE多层架构下,所谓的架构分解主要是对程序纵向的分解,但是程序结构方面是没有横向分解的。而witrix架构的一个核心就是横向存在着
CRUD模型和Biz的分解。在系统的所有实现过程中,所有CRUD操作都剥离到MDA模型中,而不需要任何代码编制。典型的,
witrix后台代码一般写在handler中,命名为handler而不是service是因为handler中负责的内容和j2ee传统上的
service有所不同,一般service总是要负责某个实体的CRUD操作,大量的findxxx代码。一般提倡的最佳实践是实现某个通用的基类,所
有service都继承该基类获得CRUD能力。但是在Witrix架构中,根本没有这一需要。Handler只要完成自己特定的功能,它不追求操作概念
在其本身的完整性。没有CRUD,
handler没有意义。但是handler之所以有意义是因为它提供了CRUD之外的操作。当CRUD成为系统一种自动进行的背景操作时,我们不再需要
明确意识到它的存在。
我们需要认识到我们最终所需要的东西可能不是规整结构的, 它可能要求对于某个规整结构进行剪裁并增补附加元素.
但是这样的规整结构不应只存在于我们的想象之中,相应的剪裁过程应该是可以增量进行, 反复进行的. 在Witrix平台中, 基本的一种图景变化是:
Witrix中不再需要从头开始构造结构, 而只要指定当前业务和背景模型之间的差异部分.
在Witrix中所写的业务代码是对核心模型的扩展。这不仅仅是概念上的,而是实际体系架构上精确的体现。CRUD作为独立的模型吸收了系统中大量的变
化。整个模型内核是采用通用方式借助meta实现功能,并不涉及到特定于业务的类。对于那些我们已经掌握的知识,
Witrix提供了超越对象继承,AOP和组件重用的结构抽取手段, 使得知识可以稳步积累.
数学中存在两种基本的分解方式, 一种是加性分解 (a,b) + (c, d) => (a,b,c,d), 另一种是乘性分解
(a,b) X (c, d) => (ac,bc,ad,bd), 它也对应于张量(Tensor)运算. 在群论(Group
Theory)中,直积对于复杂性的化简至关重要,它的重要性要远在加和之上。实际上AOP操作类似于直积分解, 只是它的能力尚未得到充分的探索。
在Witrix中,biz的作用在感觉上很象是陪集(coset)运算:CURD *
biz。不同的biz作用到同样的CRUD模型上产生不同的操作集合,而所有biz组成的集合构成独立的整体。
Witrix平台中作为内核的MDA部分首先是物理模型驱动, 而不是逻辑模型或者对象模型驱动.
我们通过在物理模型上标注的方法恢复部分对象模型的信息, 但是我们并不试图把整个软件建立为模型. 所建立的仅仅是整个程序模型的内核.
http://canonical.javaeye.com/blog/29412 一般业内鼓吹的所谓MDA成功的关键是要提高抽象层次。
但是陪集是更抽象吗。 正规子群更抽象吗。 它们只是系统的指标性表征,使对信息的distill, 是更容易理解的一个侧面而已,
抽象性并不是一个真正的目标。很多时候我们需要的是把系统降维到某个子空间中,形成一种可控性。 但是这个子空间并不一定是更抽象的。
群作为基本的代数系,一个本质特征是具有逆元。Witrix的MDA中明确定义了逆元结构,即界面上的元素 empty =
buttonA + (-buttonA),这一分解公式应用到后台 OpA = Update * (-Update) *
OpA。假设我们已经建立了结构X, 现在需要建立一个与X略有不同的结构Y
X = a + b + c
Y = a + d + c = (a + b + c) - b + d = X - b + d
虽然Y的两种构造方式在数学上是等价的,
但在物理上并不等价。第一种方式对原有系统进行分解后再组装,而第二种方式没有打破原有的东西,不需要拆分。拆分总是可能存在问题的,正如你把所有电脑零
件拆装下来再装上很可能会发现多出几个零件。一般情况下第二种方式的构建成本要低. 特别是当一切都纠缠在一起的时候,
一种细粒度的逆元结构对于一种试图重用的结构是非常关键的.
可重用性的障碍不仅仅是来自于无法追加新的功能,很多时候也在于无法屏蔽原先已经提供的功能。目前所有的设计原则都未能适时识别出逆元的重要性。所有的设
计教条其实所指的方向都是加和, 如何分解出更小的组元, 如何把它们加和在一起, 如何从细部开始进行重新构建,
而不是说依赖于现有已经形成的宏观结构, 如何进行细粒度的调整. 所谓的AOP技术思考的关键点也在于如何给系统增加功能,
很少有人想到场景是为系统减少功能并把这种概念大规模正式应用的, 虽然说AOP已经在某种程度上具有了这种能力,
但是真正应用它仍然需要对AOP进行进一步的诠释. 当然,现在的软件业连基本结构的构造问题都没有完全搞清楚, 更别提所谓结构稳定性的问题了.
从物理上说,Y = X - b +
d的分解方式具有特殊的意味。如果没有逆元,我们必然需要分解。但是如果发掘了背景这一概念,在逆元运算下,对背景不是分解让其成为可见的部分,而是采用
追加的,增删的方法对背景结构进行修正,则我们有可能在没有完整背景知识的情况下,独立的理解局部变化的结构。即背景是透明的,知识成为局部的。
Witrix试图提供的一种图景是永远只写代码片断,而所有的代码片断组合在一起又构成一个可理解的整体。这个整体可以独立理解,不需要额外的结构元素。
Witrix架构所追求的是在不完全信息下建模,不进行整体建模。整体模型 + 不断变化的局部修正 构成
最终模型。平台技术的目标是让一切应该发生的自动发生,让一切不该发生的无法发生。这一模型的构建并不是trivial的,在概念和实现方面都要作出很多
的努力。
题外:
今天中午参加同学的婚礼, 席间和一个与同方有些渊源的同学谈到ezOne的现状, 大致的评语是: 垃圾, 自己人也不用. 听来也让人有些感叹. 中国原创的技术总是欺骗的代名词, 这一断言不应总是得到证实.
建筑学的隐喻在软件业中一直很流行。开发软件和建筑楼房从某种意义上说都是一种构造过程,而建筑学的成熟无疑让很多软件工程师非常羡慕。Design Pattern的始作俑者坦承受到建筑学著作的影响更是让一些人对建筑学的精深顶礼膜拜不已。不过,建筑决不只是表面上的形象/功能设计,也决不是民工就可以干的活计,在现代建筑设计背后是土木工程的蓬勃发展。正是框架结构的出现和材料工艺的进步才使得批量生产的大型现代建筑成为可能。
虽然建筑学有着它不为人知的复杂性的一面,但是与软件开发相比,它也有着简单贫瘠的一面。现在架构师言必称设计模式,但是估计很少有人读过Christopher Alexander的原著"A Pattern Language"。在Alexander的概念中,所谓的模式"describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice". 关键在于这些模式在建筑学中往往映射为某种独立的原子化的实体(entity), 因此可以把它们作为一种语言的基础组分,构成所谓的Pattern Language. 例如现在要开发一个门廊,你可以从"私家的沿街露台(140)"开始,在"有阳光的地方(161)"做成一个"有围合的户外小空间(163)", 选择"6英尺深的阳台(167)", 保留"小路和标志物(120)", 注意"天花高度变化(190)"和"角柱(212)"的位置,在"各式座椅(251)"的旁边安排一个"高花台(245)". Alexander共定义了253个模式,括号中的便是模式的编号。很明显,物理空间的不可重入性直接规范了建筑设计空间的有限性。
在软件设计中,类似VB/Delphi的可视化界面设计的操作过程与此类似:理想情况下界面开发基本就是用各种界面元素填满一个Form的过程。但是软件的一个本质复杂性在于它的基本结构单元是函数,而设计空间中同一个功能点对应的实现函数的个数和形式并不存在有限性的约束,函数的组合形式也不是线性延展的。建筑基本上依赖的是静力学,而软件无疑需要用动力学来刻画。即使是界面开发,我们所关注的也决不仅仅是静态摆放问题,而更重要的往往是界面元素动态相关和动态变化的问题。
在Web开发领域,一直有人鼓吹模仿VB/Delphi的快速开发工具,但是我并不认为这其中的设计哲学是与软件的本质相匹配的。软件中所蕴含的无限的动态变化不应该仅仅通过有限的配置过程来应对,我们需要更加强大的结构抽象和结构构建手段。
程序中大量的工作其实都是在定义结构以及结构之间的关系. 一般情况下我们应该识别出结构,并把它们封装到函数,对象和组件中去. 但是封装并不永远都是有利的. 将某个结构独立出来, 在某种程度上也就割裂了它和其他元素之间的关系, 这会引发结构融合的障碍, 也会造成思维上的负担. 事实上如果程序整体具有足够的可理解性和概念稳定性, 我们并不需要独立识别出什么子部分. 一个简单的例子是数组循环. 一般情况下我们应该尽量把循环查找等操作封装到函数中, 避免多重循环嵌套时产生过于复杂的代码块. 但是如果数组或者语言本身提供了each, map等函数式操作符,则这种封装需求就大大减弱了.
随着系统结构的日益复杂化, 在系统中会积累大量的背景知识.此时当我们需要完成一个功能的的时候, 往往不再需要指定所有的信息, 而只需要指定背景知识之外的部分信息即可. 例如在界面上通过一个分页表格来显示实体列表这样一个功能, 在Witrix平台中通过模型驱动的标准页面即可自动完成. 一般的定制需求往往是过滤显示部分数据, 在表格行上增加一些操作按钮, 定制表格的表头等. Witrix平台实现这些需求并不需要封装出一个独立的表格组件, 调用它的属性修改方法等, 而是把定制部分嵌入到BizFlow的配置中, 这里并没有明确的结构界限.
<biz id="default">
<filter>
<eq name="status" value="1" />
<filter>
<tpls>
<tpl id="thead>
<thead>
<tr rowspan="2">...</tr>
<tr>...</tr>
</thead>
</tpl>
<tpl id="rowOps">
<ui:FlatButton .../>
</tpl>
</tpls>
其他与表格无关的信息
</biz>
注意到对于我们理解业务而言, 我们并不需要知道表格具有分页, 排序, 隔行变色等功能. 所有和业务相关的代码聚集到BizFlow文件中, 它们构成一个可以独立理解的整体, 在此过程中也通过背景知识实现了大量结构的消解.
传统上报表引擎主要完成两项工作:结构描述和结构转换。一般报表设计人员通过可视化设计工具完成对报表结构的描述,然后报表引擎根据这些描述生成不同格式的报表文件,如PDF格式,XLS格式等。这一图景中报表设计工具扮演着关键角色,因为它不仅仅是向用户提供一个直观的界面,更重要的是配置过程本身就是一种分步骤的结构构造过程。理想的情况是用户指定报表中具体有哪些单元格,表格具体有哪些列,而在运行期报表引擎负责向单元格中填充数据。但是对于设计期只能进行动态描述,无法预先确定所有结构元素的报表(例如交叉表的列只能在执行时确定),这种报表模型就会出现问题。一般处理方式都是在报表引擎中内置所有可能的动态报表模型。无论设计工具多么复杂,其内置的原理如果是基于静态结构模型,就无法建立一种抽象机制,这样我们就只能通过重复劳动来应对众多结构类似但是略有不同的报表。
Witrix平台的报表引擎是对程序友好的,它引入了编译期结构运算,在报表编译时可以通过程序吸收大部分结构差异性。在Witrix平台中,报表制作分为三个阶段:设计期 -> 编译期 -> 运行期。报表引擎负责完成三种工作:结构描述,结构生成和结构转换。具体实现动态结构生成的过程其实非常简单。目前所有的Witrix配置文件都通过基础配置引擎进行解析,它定义了统一的dynamic和extends元机制。
<report dynamic="true">
定义了dynamic="true"的报表定义文件首先作为tpl模板文件来运行,其运行结果再作为报表格式解析。在这种模型下,报表引擎并没有内置如何把动态结构拼接出来的知识,这些知识存在于编译期,而tpl标签的抽象能力使得我们可以把复杂的报表结构生成过程抽象成简单的标签调用形式。
<report dynamic="true">
<body>
<table>
<thead>
<c:forEach var="_h" items="${cols}">
....
</table>
</body>
</report>
==>
<report dynamic="true">
<body>
<rpt:GenCrossTable tableMeta="${tableMeta}" loopVar="tableData" />
</body>
</report>
在编译期通过tpl封装可以解决大部分结构生成问题,在运行期报表引擎主要负责的结构问题就简化为数据行展开和单元格合并等确定操作。
Witrix报表引擎的另一个特点是运行期结构生成过程和结构转换过程同时进行,因此不需要在内存中构造一个完整的报表数据对象,大大减轻了内存压力。Witrix报表引擎输出的文件格式目前有html, XML格式的Word文件和XML格式的Excel文件等。每一种输出格式相当于定义了一种渲染模型,它们都是对报表模型的一种展现方式。从某种程度上说这些模型的结构都是等价的,但是完成模型转换所需要的操作往往不是局域化的。例如在html的table中某一单元格具体对应哪一列是受到其他单元格的rowspan和colspan属性影响的, 在Excel中则需要明确指定列的index属性。为了简化运行期逻辑,内置的报表模型必须提供一些冗余结构,从而兼容多种渲染模型。
元数据(meta)是描述数据的数据。它所描述的有一部分是数据本身的特性,如字段的长度,精度等,另外一部分描述的则是我们使用这些数据的可行方式和目的等。使用meta可以在程序中更加清楚的表达出我们的意图。例如现在需要在界面上显示一个列表,我们的意图未必是要在界面上显示指定的字段A, 字段B,字段C对应的列,而是"显示那些应该显示在列表中的字段"。这一看似同义反复的表述,如果采用元数据表达,则成为 <ui:PageTable fields="${dsMeta.listableFields}" />。通过使用元数据,我们可以做到系统中众多的功能可以共用实现,即通过同一个页面应用不同的meta则得到不同的最终展现,而后台一个通用的DaoWebAction通过使用meta可以完成对所有实体的操作。这也可以看作是一种复杂的策略模式的应用。
ORM(Object Relational Mapping)技术为什么是有效的?对这个问题一般的答案是ORM解决了面向对象技术和关系数据库之间的阻抗匹配问题。但是任何一种成功的技术,它的支持理由都不会是单一的。在Witrix平台的实践中,ORM的如下几个特性是关键性的: 1. 主键和对象之间的一一对应关系。在Web应用中,前台浏览器持有的只能是对象的某种表示, 因此一种locator机制是最基础的要求。 2. Container结构。Container所拥有的信息包括其所有元素以及元素之间的关系。因为Container具有全局知识,所以它可以解决对象图中的循环依赖问题。正是因为session中一级缓存的存在,才能在实体延迟加载的情况下保证对象引用的唯一性,保证a.b.c.a == a。在程序设计中,所有支持对象图的Container结构都是non-trivial的,都必然是有价值的。例如spring容器在配置管理方面最重要的价值就在于可以管理循环依赖的对象创建过程。而在witrix平台的前台所定义的ControlManager管理器其核心作用就在于协调前台控件之间的消息响应依赖关系。从spring的配置文件可以清楚的看出,bean的声明过程是顺序进行的,但是bean的创建过程是非顺序结构的。在数学上,我们说系统的拓扑(topology)发生了变化。 A a = new A(); B b = new B(); a.setB(b); b.setA(a); 3. 对象作为复杂属性的集合。关系数据库中保存的都是原子性数据,每一列都是不可再分解的原始数据类型,数学上称之为标量(scalar). 而ORM引擎返回的直接就是嵌套的复杂数据类型,因此不再需要一个额外的造型过程.虽然总是返回全部数据列不是很优化,但是这种强制性的较粗的处理粒度使得前台程序可以有更多的选择自由. 4. 对两两关系的内蕴表达及充分利用.关系数据库中所保存的是系统分解后的表示,即关系被分解了而不是被表达了,外键对数据关系只起到一种约束作用,它对于查询语句的构建并没有直接的影响.所有数据之间的关系都必须在查询的时候明确指定出来,即调用者必须拥有数据关系的知识,而不是数据本身拥有这些知识.在如下的sql语句中 select * from a, b where a.fldA = b.fldB and a.fldC = 1 and b.fldD = 2 a.fldA = b.fldB 可以称为关联条件,而a.fldC=1可以称作是坐标条件.SQL的复杂性很大程度上来源于我们频繁的需要在各处指定完全一样的关联条件而无法把它们抽象成可复用的组分.在ORM所提供的对象空间中,对象之间的两两关联只要指定一次,就可以在增删改查等各种操作过程中起到作用,特别是在对象查询语句中,可以通过两两关联自动推导出多实体之间的关联关系,虽然推导出的结果未必是最优化的. select from a where a.fldC = 1 and a.b.fldD = 2 在Witrix平台中,我们做了大量的工作以确保对象上的复合属性(例如a.b.fldD)和本征属性(例如a.fldC)在使用上是完全等价的.这些工作的结果并不仅仅是减少了一些应用层的代码量,它使得系统结构发生了深刻的变化.复合属性把单表模型推进到了业务主题模型,使得单一业务对象可以聚集某一范围内的相关结构信息,这才使得witrix的模型分解技术成为可能。因此在我们看来,HQL是hibernate价值的集中体现. 5. POJO提供了纯粹的first class的持久化结构空间.采用POJO结构可以充分利用现有语言及开发工具中的一系列基础设施,大大降低了持久化结构的构造成本.而透明的get/set操作使得我们可以完全基于相对知识对持久化结构进行变换,在完全不依赖外部环境信息(例如数据库连接和ResultSet界面)的情况下解决系统的主要业务结构问题.这一切成为可能在技术上都源于AOP(Aspect Oriented Programming)所引入的重新诠释的能力,它使得我们可以将对象上的某种相对操作重新诠释为对数据库的相应操作. http://canonical.javaeye.com/blog/37064 6. Entity具有活动能力.Entity并不是完全被动的数据容器,而是可以定义复杂动作的对象.在Witrix平台中,后台程序大致具有如下结构 Entity ---- 结构问题 Handler ---- 业务动作定义 BizFlow ---- 动力学问题 Handler类似于J2EE中传统的Service层,只是一般实现的方法要少的多.而BizFlow是某种结合了界面表示的流程引擎.基于实体结构使得系统在细粒度处具有某种活动能力,便于我们构造一些局部结构来解决问题,因而也就缓解了大量操作在Service层的堆积过程,有利于维护系统整体结构的对称性. ----------- ==> -------------|-- ----------- |-- 通过对于ORM技术的理论分析,Witrix平台采取了一些和一般J2EE架构不同的设计.实际上目前J2EE架构下的常见的DAO模式在使用ORM技术的情况下往往不是合适的选择,因为DAO的一般设计是封装某个实体相关的操作,它直接破坏了ORM的container结构。原先我们只需要EntityManager.get(entityClass, id)这一通用方法既可得到各种数据实体对象,而现在需要对每种实体调用不同的Dao函数,显然这是对系统结构的重大破坏。在Witrix平台的设计中没有独立的DAO层,它通过通用的EntityDao统一完成所有对象的存取过程,而不是每个XXXManager继承公共的Dao类。即整个系统架构中尽量维护数据存取过程的统一性而不是实现它的分散化。 在Witrix平台的Workflow引擎,Wiki引擎等模块的设计中,IWorkflowStore和IWikiStore等类的设计类似于DAO模式,是对存取方式的一种封装。在比较复杂的模块中,对于存取逻辑做出一定的封装是需要的。但是注意到Store类的设计和实体框架的设计相比,其结构可分解性要相差很多,它基本上只提供对外的服务接口。如果我们能够对于文件系统等存储设施作出充分多的工作,我们一样可以对于非关系数据库的某些存取形式完成Container结构,只是这个工作量过大,而我们一般并不需要对非通用的存取结构掌握如此充分的知识。 实体结构隐含的扩展了系统的同时性视图,a.b.c.a == a 所隐含表达的事实是a,b,c是同时存在的. http://canonical.javaeye.com/blog/33797 在某些时候,例如当我们需要将系统结构顺序化,序列化的时候,这种同时性会成为一种障碍.因此Witrix平台中数据同步所使用的ETL(Extract Transform Load)引擎是基于表结构(分解后信息)的,而不是基于实体结构(关联信息)的.实际上,关系模型在某种意义上是系统分解后的必然结果,因此随着我们对系统的理解的粒度要求越来越精细,很可能最终需要我们明确意识到关系对象本身的存在性,最终实体模型会非常近似于关系模型.只不过在实体模型级列中我们选择的余地更大,关系模型可以看作是它的某种极限.理想的情况是在不同的时刻我们可以使用不同的关系抽象,只是受限于目前的实现技术,在系统构建时刻基础的关系结构往往就被固化下来.
JSF(Java Server Faces)技术从发布时间上看已经是一种比较古旧的技术了,但是目前仍未能成为主流的开发实践。从我知道这种技术开始, 我对它的判断就与我最早对于EJB的判断一样, 它们都在某种程度上捕获了真正的需求,但是因为它们自身诡异的技术路线.我很怀疑是否这些标准制定者故布疑阵, 便如Microsoft的OLE技术一样, 故意抛出一个错误的方向, 将大批组件开发商带入死局. JSF技术是一种双重的存在:它首先是一种标准,然后也提供了一种缺省的实现。但是从这两方面,我都无法看到JSF的未来。 从设计上说,强类型的视图模型对象层与Witrix的架构设计原则严重冲突。Witrix的基本架构是浏览器和后台服务器通过具有显明语义的url实现两分,这也是所谓REST风格的一种内在要求。隐蔽了链接的技术破坏了基本的web超链模型. 为了获得那么一点点结构控制能力, 做出这样的抽象是不合适的.JSF的配置模型继承了structs的传统,仍然是那样的冗长繁杂。我们是否真的需要这些配置文件,还是希望像ROR那样在代码中直接完成一切? 不能在标准的浏览器中预览. 可以说创造了一个JSF IDE的市场, 但是这无疑是一个无聊的市场. 现在有一些备选的方案, 如Facelets, 使得jsf可以采用属性语法, 但是只要想想仅仅为了这么一点小小的修正所需要付出的开发量就足以让人崩溃。 JSF提供了组件级别的事件响应机制,因此似乎是AJAX应用的理想场所.但从Witrix平台的开发实践来看,JSF对于AJAX的使用是受限制的,有着很大局限性的.组件事件响应并不一定要采取JSF那种体系结构. 从实现角度上说,基于jsp tag可以说是JSF的致命弱点之一. jsp tag从设计之始就一直是未经过实践考量,其设计无法支撑复杂的控件架构. 特别是早期JSF与标准的JSP tag不能互通实际上是明显的设计缺陷, 而且性能问题是内置在该设计中的. 现在虽经多个版本的不断补救, 但是为了兼容性, JSP Tag负担过重, 它始终是基于文本处理模型,实际上不可能有什么本质性的进步. JSP tag模型过分孱弱必然造成JSF设计中大量处理过程堆叠到界面对象层,更加剧了JSF的模型复杂度和性能瓶颈。 实际上根据Witrix平台中tpl模板技术的设计经验,大量界面构建过程是可以在模板层以直观的方式完成的,而不需要借助视图模型对象。 所有问题的一个集中体现就是增加一个新的JSF组件绝对不是一件平凡的事情.如果有一天这个问题可以得到解决,那时的JSF从思想和实现上都必然和现在的JSF有着本质性的区别.
REST(Representational State Transfer) 是Roy Thoms Fielding在其博士论文中所提出的一种架构风格(Architectual Style)。 http://roy.gbiv.com/pubs/dissertation/top.htm http://www.redsaga.com/opendoc/REST_cn.pdf 可以说它也体现了整个互联网架构的基本设计原则。随着AJAX等技术的发展,互联网的资源语义逐渐得以恢复,最近对于REST的讨论也热烈起来。 http://www.ibm.com/developerworks/cn/web/wa-ajaxarch/ 在Fielding的论文中对REST有如下陈述: The name “Representational State Transfer” is intended to evoke an image of how a well-designed Web application behaves: a network of web pages (a virtual state-machine), where the user progresses through the application by selecting links (state transitions), resulting in the next page (representing the next state of the application) being transferred to the user and rendered for their use. 点击超链接之后,服务器端返回资源的某种表示(Resource Representation), 客户端的状态(Representational State)随之发生迁移(transition),在服务器端返回的表示中存在着到其他状态的迁移链接,使得我们始终自动具有进一步迁移的能力(这和ajax中所谓返回纯粹数据其实是不同的). 这种图景其实也正是Tim Berners-Lee最早所设想的web的图景。因为REST是对互联网架构设计的一种抽象,因此所谓的Restful其实就是尽量符合现有Web标准。从概念上说,REST所关注的主要还是分布式资源信息交换,而不是对复杂的业务逻辑进行抽象。遵循REST将会使整个互联网受益,确保路由器,缓存,代理,浏览器,服务器等各个组件可以协同工作,它的意义并不是针对单个应用程序的。不过与witrix体系架构相对比,我们还是可以发现REST架构风格对于一般web程序的参考价值。 1. 通过唯一的,确定的url来标定可访问的资源。url定义了后台资源的访问界面,明确区分了浏览器和服务器两个分离的状态空间。这种两分的架构约束是witrix平台最重要的设计原则。在witrix平台中,Jsplet, BizFlow, PageFlow,AuthMap等技术完全体现在url的精化设计上,系统中所涉及到的所有关键概念都对应于url中的明确组分。而JSF等技术通过对象封装模糊了资源的访问界面,迫使我们只能通过一个错综复杂的对象包络来理解系统的交互过程。 2. REST强调整个交互图景中所有需要的相关信息都是in band的,而且语义上不同的部分都对应于特定的语法上不同的部分。例如为了保证url的不透明性和稳定性,一些信息通过http协议的header来传递。这里强调的语义自明性实际上是脱离具体知识体系的形式上的可区分性。在现有的服务器端程序中, 因为直接接触到的是已经解析好的parameter集合, 因此Witrix平台中关注的是在参数集合中识别出特定的形式结构, 这通过EngineURL对象实现. 3. HTTP中明确区分了GET/POST/PUT/DELETE四种标准操作, 并对它们的语义进行了精确的限定. 而所谓的RPC(Remote Procedure Call)与此是不同的. RPC上承载的方法数可以是无限多的,但是这种无限多的操作反而从某种层面上说是简单的,是对称的,没有可区分性。而在REST中,在确定资源这一限制下,我们可以区分出GET/POST/DELETE/PUT这四种有限但是不同的语义。 是否这四种语义构成完备的操作空间?从某种意义上说, 这四种操作覆盖了资源的整个生命周期, 它自然满足了某种完备性. 但是抽象意义上的完备性并不意味着它在物理层面上也是完备的. 我们都知道二维空间是完备的,但是二维描述并不是三维空间的合适表达. 在Witrix体系下,WebAction所提供的完备的操作集合包括Query, ViewDetail, Update, Add和BizAction. BizAction是个特定的扩展,所有不便于直接归类到CRUD操作的操作都可以归类到这一Action的名义下. Witrix架构中把CRUD看作是一个更大的Action空间的一个子空间, 对此子空间的结构进行了细致的划分, 所关注的重点是对部分信息的更明确的表达, 而不是对程序整体的建模. 区分GET/POST/PUT/DELETE四种操作, 最重要的意义在于定义了GET和其他操作的区别. http://www.w3.org/DesignIssues/Axioms.html a. in HTTP, GET must not have side effects b. in HTTP, anything which does not have side-effects should use GET 在GET这种语义下, 才可能建立独立的cache组件, 而正是因为cache的存在, 才使得web成为infomation space而不是computing program. 而在Witrix平台中, 很多通用机制的建立(例如精确到数据行的访问权限)都需要对于CRUD做出精细的区分, 而不仅仅是识别出GET操作. 4. REST中虽然定义了具有概念稳定性的url, 但是因为操作集合有限,而且强调服务器端的无状态性, 因此一般认为这种结构并不是面向对象的. 不过,在我看来,面向对象首先意味着一组相关性的聚集, 实际上从语义层面上看, REST与witrix平台的jsplet框架非常接近, 最大的差别在于服务器端明确定义的thisObj---this指针. 服务器端的无状态性对于网站应用而言是必要的, 但是对于复杂的企业应用而言并不是合适的约束. 5. 对整个互联网应用而言,URI应该是不透明的,它的结构对互联网中间应用而言应该是不暴露的. 资源的结构是与整个互联网架构无关的. 最近业内鼓吹REST的时候往往也推崇所谓beautify的url, 例如 http://www.my.com/blog/3/comment/1. 这种结构不过是维护url稳定性的一种副产品, 在我看来, 它对于REST而言并不是必需的. 不过根据这些年关系数据库应用积累的经验,识别集合和集合中的个体是一种非常通用的场景,因此我们可能规范化这种结构,甚至搜索引擎等外部组件也可能理解这种语义,从而实现更加复杂的语义网络。在现有的所谓支持REST的web框架中, 往往支持这种规范化的url直接映射到后台的响应函数上. https://cetia4.dev.java.net/files/documents/5545/38989/cetia4_tutorial.pdf. 例如在cetia4框架中, GET /blog/3将会被映射到后台函数 String render(RenderContext context, int id)函数上. 在witrix平台中, 缺省并不采用beautify的url, 但是因为对于语法成分具有明确的定义, objectEvent=ViewDetail&id=3这样的url将映射到后台biz-flow中的action段. <action id="ViewDetail-default"> <source> 在这里直接拿到entity对象,而不是id值 </source> </action> 在action中我们直接接触到的不是id值,而是id值相对应的实体对象本身. 对于objectEvent=Remove, 我们可能一次删除多条记录, 则在后台bizflow中action=Remove-default段将会被调用多次,每次处理一个实体. 这些自动处理机制都离不开对于标准化url组分的明确理解. 在网站应用中, 我们一般也通过url rewrite机制来支持简化的层级url. 但是这种根据位置来确定语义成分的url格式其实也存在着很大的局限性, 在cetia4的映射中很多时候都会出现含混的情况. 而且因为资源是抽象的, 页面中的相对路径计算会出现一定的困难. 在witrix平台中, 通过tpl模板语言的标签增强机制, 我们重新定义了页面中的相对路径计算规则, 从而大大简化了资源管理问题. 在tpl中相对路径计算永远是基于当前模板文件的, 例如对于通过<c:include src="subdir/included.tpl" />引入的子页面included.tpl, 在其中的<script src="included.js" />等使用相对路径的语句会在编译期被转换为使用绝对路径, 生成<script src="/contextPath/root/subdir/included.js" >等语句. 6. 一旦url格式被规范化, 我们就可以基于它在应用程序中发展很多全局结构. 例如在cetia4中, 可以建立全局的navigation模型, 并提供了一个breadcrumb 导航栏. 这是一种全局的链接管理机制,在一定程度上实现了导航信息压缩. 但是因为其没有对象结构支撑, 与Witrix平台的PageFlow技术相比, cetia4的方式不过是非常原始的一级策略, 它对于对象生命周期的管理也是过于简陋的.http://canonical.javaeye.com/blog/32552 7. REST中强调generic interface 而不是强调custom interface. 实际上目前业内的对象化方案很多时候都沉迷于提供特定结构的构造方法, 很多面向对象框架本身就在构造各式各样的特定结构。一种通用的结构只存在于概念中,是复杂结构背后的事情。但是在很多情况下, generic interface无论在实现成本还是概念成本上都是更加低廉的. 在witrix平台中, jsplet框架所实现的对象化就是一种generic方式的,弱类型化的, 而不是强类型的对象结构的。 8. 关于REST的另一个有趣的问题是如果大量使用HTTP内置特性,是否我们的应用将严格绑定到http协议上,不再是所谓的protocol independence。不过我们真的需要同一语义模型的不同实现吗.
今天adun给我讲了一个他所谓可退化的设计,在我看来问题还是多多。从直观的角度上说,在java中声明一个具有多个参数的函数,调用的时候对于不需要用到的参数都传入null, 这不是理想的可退化场景。所谓的退化不仅仅是概念层面的,不仅仅是关于语义的,很大程度上它也是形式上的,是关于语法结构的。 理想的退化场景是尽量维持形式/结构稳定性的情况下实现诠释范围的缩减,在任何层面上都不需要知道超出当前需要的信息。而如果我们被要求必须传入自己实际上不需要使用的参数,则必然存在着一定程度上的信息泄漏。一个朴素的看法应该是,当我们需要它是一个参数的时候它就是一个参数,当我们需要它是三个参数的时候它就是三个参数。对于系统形式结构的有效规划是实现可退化性的前提条件。
描述所关注的是“what”,而运行所关注的是“how”。在现代软件开发中,描述信息作占的比重日益加大。甚至一种极端的倾向是把所有业务逻辑都写在各种格式的配置文件中. 配置文件目前多采用xml格式,它的优点是自说明的:属性名直接标示了其基本含义,但是这也在一定程度上加重了命名的负担, 造成了配置文件的臃肿。因为在普通的程序语言中,可以用来传递信息的结构更加丰富,例如参数的相对位置,参数类型, 匿名函数, 指针引用等。而一般配置文件中没有定义合适的继承,封装等抽象机制,很难如同普通程序语言那样进行有效的结构压缩。 在很多灵活的弱类型语言中,借助各式语法糖(syntax sugar)可以实现描述性的运行结构, 或者可以看作是构造性的描述, 它在部分程度上消解了描述的诠释问题, 不需要额外的解释器即可实现描述结构的解析. 这有些类似于编译理论中的语法制导翻译, 在动态结构组装方面具有明显的优势. http://www.blogjava.net/canonical/articles/19697.html. 但是独立的描述信息仍然是有着重要作用的, 关键是作为元数据存在的描述信息可以以多种方式被使用, 并可以被部分使用. 此外一些特殊设计的描述文件可以很自然的汇集系统各个方面的信息到同一层面加以展示,而一个通用语言无论语法如何灵活, 抽象能力如何强大, 毕竟受限于先天的结构, 要做到这一点还是不现实的. 在witrix平台中配置文件的设计一般是综合考虑静态描述和动态调整的需要, 在设计上分成静态描述段和动态运行的init段, 系统将确保init段中的tpl代码会在适当的时候被调用.
在商业产品开发中,如何有效的控制同一产品的多个衍生版本是一个非常重要的问题。客户的需求是多样化,差异化的。这些差异有些很小,可以通过参数配置,资源装载,skin切换等方式加以吸收,而有些则要求对界面布局和程序逻辑等作出较大调整。Witrix开发平台在系统基础架构方面为程序的客户化提供了有力的支持。 1. 多版本控制的关键首先在于系统良好的模块划分。因此Witrix平台的beans,auth-map(权限归约规则)等配置文件格式都支持import/include等基础的分解策略,字符串资源和错误码映射等支持多重定义文件,而对于sql.xml(外部sql语句定义), meta.xml, biz.xml, hbm.xml等配置文件采用分模块动态装载机制。 2. 在Witrix系统中定义了一个特殊的custom目录,规定了一般性的覆盖规则:custom目录作为系统根目录的影子目录,如果custom目录下存在同名文件,则优先装载custom目录下的文件。例如,如果custom目录下存在/_config/my/my.biz.xml文件,同时在根目录下也存在/_config/my/my.biz.xml文件, 则实际装载的是custom目录下的实现。这里的一个关键在于只有meta.xml(元数据),biz.xml(BizFlow描述文件),.lib.xml(tpl模板库)等具有一定完整性的文件才支持custom机制,而并不是所有资源都采用custom机制。如果每一个tpl文件,css文件,js文件等都优先从custom目录下装载,则很快就会出现循环引用,相对路径计算将会变得非常混乱,更重要的是我们将无法定义资源删除语义。 3. 元数据文件,BizFlow描述文件,PageFlow描述文件等都支持复杂的extends机制,使得我们在扩展时只需要对于系统差异部分进行描述,而不是大段拷贝代码。 4. tpl模板库和sql-map机制等采用的是追加覆盖策略。例如custom目录下的ui.xml标签库文件并不是直接覆盖系统根目录下的ui.xml文件,而是按照标签名进行细粒度的覆盖。系统编译时会自动检查覆盖标签的所有参数要求和原标签相兼容(例如允许增加参数而不允许减少参数),确保所有引用到原标签的tpl代码仍然有效。实际上整个witrix平台多版本扩展机制的一个设计目标就是确保平台主系统向各个分支产品的单向信息流动。在具体的表现上就是我们随时可以拷贝平台主系统覆盖到分支产品的相应目录,所有扩展实现与主系统实现保持分离状态。当然为了保持设计的弹性,系统中也定义了开关参数用来有选择的跳过一致性检查。
命名(Naming)无疑是人们认识世界最基本的手段之一。从软件技术的发展中我们也可以观察到命名技术的不断深化。 1. 助记的名:汇编之中我们有了move/push/pop这样的指令,所面对的不再是010101这样的同质的数字世界。变量也逐渐可以拥有自己的名字,甚至多个名字。在C语言中指针的概念被进一步抽象化,使得我们可以为任意内存地址起一个可读的名字。我们甚至渐渐忘怀了pStruct是指针的名字,而直接把它等同于指针所指的内容本身。 2. 局部的名:函数(例程)概念的出现把局部名称引入系统,从此精细结构的发展才成为可能。 3. 多义的名:面向对象的出现可以看作是命名技术的一种重大进展,它可以把一组相关的数据和函数放在一起起个名字。继承概念为名引入了多义性。通过虚拟函数表所实现的lazy-binding部分松动了对象的名和实之间的指称关系。现在一些所谓dynamic dispatch技术,依然是顽强的希望在同一名下,纳入更多实的变化。 4. 特指的名:面向对象技术创造一个特殊的名---this指针。它是一种约定了的固化了的局部名称。使用this指针使得我们区分了领域(domain)的内外。在domain外对象可以有各种称谓,而domain内我们直接通过this直接触及到对象的实体。在javascript这样的动态语言中,函数和this指针是动态绑定的。在某种意义上,这意味着一个操作所依赖的domain知识可以是动态变化的。 5. 相对的名:面向对象技术所创造的知识相对化概念的一个直接体现是命名的相对化。一个函数它的具体含义不再是绝对的,而是相对于this指针的。因此我们不再采用user_load, book_load这样的全称的函数名, 而只定义load这样的具有依赖性的函数。在面向对象的理想操作图景下,首先应该是通过一个整体的参数直接区分出多个大的情景,然后在每个特定的情景下分别调用相对函数进行操作。在模板(template)技术或者动态语言中,这种相对性可以得到更加充分的发挥。因为在泛型或者弱类型语言中,我们需要的只是对象提供特定名称的函数或属性而已。 6. 持久的名:在早期的语言中,名只在编译时刻存在。在编译出的二进制代码中,名所提供的丰富的描述空间不复存在,我们所有的只是同质性的二机制地址而已。而在现代语言中,反射已经成为了不可或缺的技术,它使得我们在运行时刻仍然可以拥有复杂的描述结构。 7. 分离的名:在一般的程序中,我们早已习惯了变量名直接指代实际可操作的对象本身,名的问题显得太平庸以至于大家似乎忽略了它的存在。但是在web体系架构下, 因为存在着浏览器和服务器这样两分的状态空间, 名成为两个系统交互的直接手段,名的重要性也重新凸显出来。只有在一个封闭的container中,才能通过名字解耦. 因此web架构设计的一个核心问题是构建出各种各样的全局的container. 浏览器前端技术的一个本质性困难即在于多个浏览器窗口之间没有共享的全局对象空间,因而很难在前台独立建立container结构。 在witrix平台的jsplet框架中,在前台我们通过如下url来访问后台 /view.jsp?objectName=MyObj&$bizId=test&objectEvent=ViewDetail&id=1 MyObj这一参数标定了BeanContainer中的一个Java对象, $bizId参数指定应用某个Aspect到此对象上,objectEvent参数映射到WebAction上的一个java方法,而EntityManager最后负责把id映射到具体的实体对象。当我们在后台需要编制代码的时候,entity对象已在手边。 8. 名的结构:当名越来越多的时候,我们需要对它们进行有序的组织。名字空间(namespace)所采用的树形结构可以说是最直接的一种组织方式。这一结构不仅仅可以用于描述,同时可以用于控制。
软件这个领域中传统上占优势的是自vonNeumann以降的数学视角,计算问题是其思想内核,而函数式语言无疑是其比较贴切的表现。但是仅有数学,我们对于世界的认识是不充分的。有这样一个笑话。烧一壶水的完整步骤如下:1.向空壶中注满水 2.放到火炉上 3.烧到冒泡。现在有半壶水,求解烧水的步骤。数学家的回答是直接把半壶水倒掉,然后宣称问题已经解决,因为它已经被归结为第一个问题。实际上数学的视角直接限制了某些命题进入研究者的领域。现在业界占主流的面向对象技术,并不像是理论界的自然创造,它的思想来源更像是来自于开发窗口系统的工程实践。 http://gagne.homedns.org/~tgagne/contrib/EarlyHistoryST.html. 面向对象技术的核心是描述问题,它试图实现业务概念和业务关系在程序中的直接表达,所谓更贴近人的思维方式。但是从面向对象的实际操作过程我们即可得知,这里所谓的贴近只是贴近人们的常识而已。常识是有意义的,但也是浅薄的。当程序功能变得愈加复杂,程序的结构不再那么直观的时候,我们从面向对象理论得到的支持并不如最初它宣称的那样的巨大。早期面向对象技术所提出的枚举系统中所有名词和动词的设计方法现在看起来无疑是非常的幼稚。如何确定一个系统中到底需要定义多少个对象,以及如何确定它们之间错综复杂的交互关系,这些并不是通过我们的业务常识能够确定的。 在具体的,特定的常识与抽象的数学之间,存在着所谓的物理学。常识总是和一定的“有意义”的场景绑定着,可以直观的理解。而数学则蒸馏出一些纯粹的符号,试图与所有预置的意义划清界线。物理学是折衷主义的,或者说是非常狡诈的。它选择了只在需要的时候诠释。在物理学的推导中,大量的中间过程都是数学性的,难以寻找到明确的物理含义的,但是我们无视它,只对某个最终结论加以解释。事实就是公式千千万,但是我们却只对其中的某些说:嗯,这里面有物理。与此对应,在软件开发中,将直观的业务需求映射到通用的程序语言实现时并不是那么直接的,在这之间可以存在着超过一般人想象的与特定业务无关的厚重的技术层。在这个层面中可以定义出非常复杂的交互模式,并在不同的特定场景下将其诠释为不同的业务应用。这也正是平台技术赖以生存的基础。 丰富的物理学的根基在于丰富的物质结构,它的核心是动力学。实际上单量子体系并不如想象中那样难以研究,这个领域充斥着粗鲁的线性近似,而它们的预测精度却都难以想象的高。真正的复杂性来自于简单元素所构成的复合结构,以及这些结构之间的相互作用。随着ROR这样的动态语言框架的流行,很多人把它们的成功归结为语言特性的增强,这在我看来并不是富有成果的方向。诚然,更多的语法糖(syntax sugar),更多的动态性可以降低我们构建某些复杂结构的代价,但是这种降低最多是减少一两倍代码录入量,而绝不可能是数量级上的影响。对于程序开发可以起到决定性作用的是那些复杂的大范围结构,而不是通用语言本身所提供的那些抽象的简单的局部结构本身。当然,一般人大概很少有能力做出超过一定难度的创造,一般都只是依赖现有的语言,现有的框架以直白的方式实现功能,因此很难想象可以在语法特征更少的java中轻易实现超过ROR的开发便捷性。 通用语言的发展必然是有极限的, 也必然是贫瘠的, 因为它无法利用有限场景下的信息. 而DSL(Domain Specific Language)将注意力集中在某个特定的领域(domain)中,便可以名正言顺的引入非常复杂的语法结构. 这些结构旨在本领域中具有意义,而不用担心超出应用范围后遭到"冗余设计"的诟病. 我无法想像在一种通用程序语言中会规定Witrix平台中BizFlow这样的结构设计,但是作为一种domain相关的语言结构,它的表述却无疑是非常高效的。我相信,随着我们对于程序结构的认识的不断深化, 在DSL所构建的复杂结构空间中可以发展出一些真正有趣的技术. 对于DSL存在着两个常见的误解.一是DSL是存在于特定领域的,因此它是一种受限制的语言,应该在计算能力上弱于图灵机.但其实DSL的核心在于高效的表达,在于直接存在的高阶结构,与它的形式计算能力并无关系.正如物理中Lagrange表述和Hamilton表述在数学上是等价的一样,我们的选择只在于某种情况下某种描述方式会更加方便。这里所玩的游戏更像是概念空间中的拓扑变换。 关于DSL的另一个误解是DSL的表述形式应该接近自然语言.但事实上数学符号和化学公式都是高效的DSL,它们的表达形式甚至内在逻辑都和我们的自然语言相去甚远.我们是否已经完全解决了程序问题,而只是要把这种能力向无知的客户转授?目前在程序编码的过程中我们仍然面临着大量未决的问题,程序员应该是DSL的直接受益者.此外,西人对于语言的认知是偏狭的,因为他们眼中的language只有拉丁语系和日耳曼语系,而不知道这个世界上还存在着不符合西文语法的汉语。按照朱光潜的诗论,西人长时间认为诗是不宜于写景状物的,因为语言是串行的,因而只适于按照步骤叙事,却不了解汉语的自由组合形式和丰富的单字表现力可以轻易捕获微妙的瞬间.
最近D语言发布了1.0版,这是一个由编译器开发者所设计的编译语言,语法类似C++, 但是针对C++的弊病作了大量修正,并增加了很多现代特征,其中还是有一些新意在其中的。http://www.digitalmars.com/d/overview.html 我对其比较感兴趣的部分是D语言明确提出的编译期运行的概念。虽然C++让大众了解了meta programming技术,很多人因此认为meta programming的威力在于类型演算,但是在我看来meta programming的真正作用在于在编译期可以“动态”产生或调整代码结构。它依赖于类型是因为我们只能利用类型来承载一些额外信息, 这无疑也是对于类型的一种滥用。在D语言中template所接受的不仅仅是类型, 或者类型的类型,它可以是任何符号. template MixInAttr(T, char[] name){ mixin(" T _" ~ name ~";"); mixin(" T "~name~"(){ return _"~name~"; }"); mixin(" void "~name~"(T v){ _"~name~" = v;}"); } template MixInOtherAttr(T, T v){ T _otherAttr = v; int otherAttr(){ return _otherAttr; } } const int myValue = 1; int addFunc(int x){ return x + 1; } class MyTest{ mixin MixInAttr!(int, "myAttr"); mixin MixInOtherAttr!(int,4); static if(addFunc(myValue) 〉 0){ int testFunc(){ return 5;} } } void main(){ auto t = new MyTest; t.myAttr = 3; int v = t.myAttr; v = t.otherAttr; t.testFunc(); } 不过现在编译期运行无疑是一个正在探索的方向, 在D语言中的使用方式并不是非常理想的, 在形式和功能实现上都存在着重大改进的可能. 在witrix平台的tpl模板语言中,我们也引入了编译期运行的概念,只是tpl的语言特征非常简单再加上xml格式特殊的规范性,编译期运行在tpl中的实现是非常直接和明确的.在tpl中定义了<cp:run〉标签,它的内容在编译期运行, 输出结果(xml字符串)将被继续编译. 在<cp:run〉中可以执行任何有效的tpl代码, 它们在编译期状态空间中运行. 例如 〈cp:run〉 〈c:forEach var="_x" items="${tagBody.children()}"〉 bla bla bla... 〈/c:forEach〉 〈/cp:run〉 在EL表达式中我们通过cp前缀实现普通调用和编译期调用的混杂. 〈cp:const class="mypkg.MyConstantClass"/〉 〈c:eval expr="${myFunc(cp:const.MY_CONST, commonVar, cp:funcInCompileTime())}" /〉 〈cp:compile test="${exprInCompileTime}"〉 any tpl ... 〈/cp:compile〉
其实当我们把改进的目光开始放到编译期的结构方面的时候, 可以做的工作是非常多的. 例如从结构上看, 继承策略相当是类结构的一种一阶替换策略. 类B(methodB) extends 类A(methodA, methodB) ==〉 类B(methodA[inA], methodB[inB]) 如果我们放弃概念层面上的纠缠,而如同template那样只关注语法结构, 则可以发展出更加复杂的替换操作. NODE_B( NODE_A( NODE_B( SUB_NODE_A1 〉〉 SUB_NODE_A1 ==〉 SUB_NODE_A1 SUB_NODE_B11 SUB_NODE_A11 SUB_NODE_B11 SUB_NODE_A12 SUB_NODE_A12 ) ) ) C++中及D语言中的template技术在某种意义上相当于是在代码结构中预留了空洞, 可以实现跨越类结构的替换填充. 只是D语言挖洞的能力无疑比C++强大的多. 如果我们考察的再细致一些, 就会发现两个语法节点的融合也是存在多种策略的. B 〉〉 A ==〉 (remove_A, replace_A, insert_B_before_A, insert_B_after_A, insert_B_into_A, insert_A_into_B) 而通过insert_A_into_B/insert_B_into_A策略既可实现所谓的AOP intercept操作. http://canonical.javaeye.com/blog/34941 在witrix平台的BizFlow技术中, 我们通过高级的结构融合策略实现为任意实体引入流程支持. 最简单的情况下我们所需要做的工作只是增加一个语法元素 〈bizflow extends="myflow"〉 引入流程意味着对界面的一系列修正, 例如增加,删除某些菜单, 同时要增加很多流程相关函数, 对某些函数实施AOP操作, 在各处引入权限控制等等, 这些都可以通过extends操作引入.
在传统上, 编译器结构是固化的, 它所编译的代码结构是固化的, 而它所编译出的代码也是固化的, 但是现代语言的各种发展正致力于在各个层面引入动态性. 编译期运行抑或是编译期结构操纵技术的引入是在一个层面上打开了潘多拉魔盒. 它让我们看到了前所未有的技术可能性, 但它无疑也是危险的,难以控制的. 我们目前的做法是尽量克制,只在局部使用, 但我相信它在很多语言中都是可以在理论上严格定义,并精确实现的, 我们最终也能够找到合适的形式来约束它的破坏性. 在这个过程中我们需要引入更多的物理化的视角。
代码生成现在已经逐渐成为软件开发中的一种标准技术,在众多的软件领域都大大减轻了我们重复劳动的工作量。程序中总是存在着这样那样的隐蔽的关联,它们无法在通用的程序语言框架下得到明确的表达,代码生成是我们突破既定的语言和框架限制的一种重要手段。但是代码生成也存在着严重的弊病,一方面一般的程序语言在设计时没有考虑到和代码生成工具的相互配合,因此生成代码是一次性的,代码生成工具无法以增量的方式修正已经生成的代码。另一方面,程序的结构是复杂的,代码生成工具一般基于某种简化的通用的程序模型(例如CRUD)来产生代码,它无法承载完整的程序结构,因此代码生成后手工调整量仍然很大,有的时候甚至为了微小的界面调整,将生成的代码修改的面目全非,无法发挥代码生成的优势。 在witrix平台中主要使用meta generation而不是code generation. meta实际上是对一种定制模型(model)的描述,它在某种意义上可以看作是完整程序的简化版本,但它本身并不意味着最终的程序结构。在witrix平台各处meta的使用都是可选的, 特别是在多变的前台页面,我们可以选择根据meta描述自动生成界面,也可以选择通过<df:Field name="字段名"/>来引用单个字段的meta数据. 在witrix平台中, meta可以看作是系统运行的内核, 它通过syncWithModel等属性与设计工具发生耦合. 当设计模型修改之后, 这种修改能够以增量的形式通过可控制(修改)的信道传播到系统各处.
目前的AOP(Aspect Oriented Programming)技术虽然以动态代码织入为核心,但是这种织入仍然是一次性的。一般在系统构造的时候(例如ClassLoader装载Class的时候)实现类和成员函数的增强。此后在运行时刻代码结构是固定的而不再发生变化。但是在真正的业务处理过程中,我们在不同的应用场景下可能要求织入不同的Aspect。例如基本的权限Aspect, 如果在不同的应用场景有不同的权限设定,则我们显然希望进入一个确定的操作场景的时候就指定一整套的权限策略,而不是在每个函数调用时刻写上一大堆的if/else(这种分离的条件判断正是AOP试图从结构上消除的)。 为了实现AOP的二级动态化,我们首先需要约定一些公共标记(坐标),便于在标记处插入Aspect Container, 其次我们需要在系统中建立一个隐蔽的信道,可以通过该信道传递一个标志符(Aspect的id),用于在各处选择特定的Aspect. 建立这种动态特性之后,我们就可以据此发展出Aspect组的概念,并实现Aspect组之间的继承关系等高阶结构,从而最大限度的限制程序结构的分散化。 Witrix平台的BizFlow设计在概念上可以看作是AOP的一种二级动态化织入设计,它通过$bizId这一特定参数来选择织入的Biz。一个BizFlow对象是一组Biz(Aspect)的集合, BizFlow可以通过extends机制实现集合之间的合并等(BizFlow实现的合并策略其实是非常复杂的)。一个简单的应用就是流程支持,例如一个普通的实体对象对应的bizflow只需要加上如下代码即可获得流程相关的代码,前台菜单等。 <bizflow extends="testflow.biz.xml">
几年前Michael Atiyah受邀在浙江大学做过一个讲演,题目是Mathematics in the 20th Century, http://www.cnw3.org/smth/Mathematics/historiesandmathmaticians/goodessays/00000035.htm, 在其中他回顾了二十世纪的主要的数学发展。被他列在第一条的进展就是From Local To Global. 在传统上,数学的主要研究对象是一些得到显性表达的局部公式,而拓扑学对于整体性的“不变性质”的研究最终将我们对于数学和物理学的理解推进到一个新的高度。我想在其他领域中,这种认识上的深化也将是一个必然的过程。随着AOP这种大范围结构操纵技术的兴起,软件技术是否也发展到了可以对程序的整体结构做一些反思的时候? 面向对象有什么用?它是在各个层面都可以使用的一种描述工具。从一些早期的文献我们可以看出一些端倪,一种整体性的均一的概念是我们迫切需要的。对象可以构成对象,Everything is Object. 只是因为我们对这些太熟悉以致于在今天看来显得有些陈腐。很多人现在津津乐道于CoC(Convention over Configuration)作为一种局部程序设计技巧所带来的可以少些一些代码的经济性,却没有看到CoC更大的作用在于在大范围内保持了程序结构的一致性,使得某些轻灵的设计可以在框架层面得以展现。目前的框架技术更多的是在各个层面各自为战,如何将同样的信息从局部传播到整体是一个耐人寻味的问题。 传统上的程序世界缺乏一些具体的技术手段使得我们可以方便的触及到程序的整体结构部分,这些整体性的关联更多的是存在于文档中,存在于我们的思想中,存在于程序表达世界之外。而AOP技术从本质上说也只是方便在各个层面实现某种局域化的抽象。 当某些东西被拘束在某个具体的孤立的点中的时候,我们似乎就可以松一口气了。但是在程序中仍然存在着大量"弱"的关联,它们很难被清晰的局域化。模型(Model)和Meta必然在程序构建的过程中扮演愈加重要的角色。AOP只是一种技术手段,它必须和更加宽广的框架技术和模型构建技术结合才能起到最大的作用。
http://partech.blogdriver.com/partech/1217744.html
partech基于AspectJ对于AOP的深入应用作了一些有益的探索。ORM的价值之一正在于通过ORM引擎对于对象上的局部操作作出持久化诠释(参见 面向对象之形式系统 )。在partech的方案中,对象删除操作(destroy)的引入显得有些勉强:因为我们需要标记一个删除的时刻,所以调用了一次空的destroy()方法。但是如果在事件驱动的应用场景中,调用时刻唾手可得,这样的问题便很少出现了。 我个人所关心的方向主要是框架层面上对于AOP概念的应用。在Witrix平台的BizFlow方案中,借助于框架技术的支撑,我们甚至连new和set 调用都不需要,例如在biz文件中只需要声明相应的事件响应函数,框架负责生成界面从用户处收集信息,负责创建对象,负责执行保存和删除操作:no new ,no set, no save, no load, no remove. java 代码 <action id="Add-default"> <source> do anything on entity to be added </source> </action> <action id="Remove-default"> </action> <action id="ViewDetail-default"> <source> entity is accessible here </source> </action> BizFlow的实作中是实现为DaoWebAction的一个interceptor。
面向对象技术中最重要的概念是什么?在面向对象理论发展的初期,几乎所有的正统声音都在鼓吹继承(inheritance)概念,言必称虚拟函数和多态性。但是依赖继承这种推导关系来构建庞大系统的弊病在实践中逐渐暴露出来,随着组件(Component)技术的发展,所谓的封装概念逐渐被推崇为面向对象思想的精华。在此过程中,接口(interface)概念作为系统细粒度正交分解的手段也逐渐发展起来。在软件系统结构的日益复杂化的今天,封装概念开始成为了质疑的对象。是否与一个概念相关的所有实现都要统一封装到一个具体的对象中?伴随着Java,C#等语言进入主流程序界的AOP(Aspect Oriented Programming)给出了不同的答案。在AOP的环境中,Object不再是牢不可破的黑箱模型,而是成为了外部嵌入的方面(Aspect)的容器。在应用AOP这种大范围结构操纵技术的对象构成体系中,封装不再是问题的核心,我们所关注的是面向对象技术中更为本原的概念:this指针。 在纯粹的技术层面上,面向对象所指的首先是一组相关性的聚集:它指代了一组相关的数据和函数。为了配合这种相关性的表达,在调用形式上发生了重大变化。从全局函数的调用方式转变到了基于对象指针的调用方式: func(this) ==> this.func(); 这里关键性的区别在于从全局性的,绝对的表达方式转变为局域化的,相对的表达方式。this指针限定了一个知识域(domain),调用对象函数是在限定知识域的情况下提供一些相对信息,即调用的时候只需要相对知识。例如现在界面上有两个按钮,其中一个跳转到编辑页面,另外一个跳转到列表页面。为了表达出这两个按钮的不同,我们只需要提供非常少的信息。 〈input value="编辑" onclick="stdPage.gotoEditPage('${pkValue}')" /〉 〈input value="列表" onclick="stdPage.gotoListPage()" /〉 所有的公共知识集中在stdPage这个对象指针中。所谓组件技术,关键点也正在于这里。基于一个给定的组件对象,我们只需要知道如何调用它的函数,就可以使系统呈现不同的表现形态。我们所关心的并不是如何构造这个知识域(对象本身),而是如何使用相对知识构造出我们最终所需的系统。封装性使我们摆脱了对系统全局知识的依赖。 从形式主义的角度上说,任何一种调用方式都只是一种表达,它的具体含义需要一个诠释的步骤。基于对象指针的调用形式直接导向了诠释的多样化:我们只需要替换this指针,就可以改变整个一组调用的具体含义。传统上,对象指针是封闭的,指代的是具体的实现,所有的信息都必须来自于对象指针本身,这造成诠释的局限性。但是在AOP的支持下,诠释可以不仅仅是源于其内的,而且可以是发自其外的。例如基于POJO的ORM框架中,我们只需要纯粹的基于对象自身的知识对其进行操作,ORM引擎通过enhance POJO对象来重新将其诠释为对数据库的持久化操作。
随着IoC(Inversion of Control)容器的流行,AOP(Apsect Oriented Programming)似乎逐渐成为了主流技术的一部分,但是除了Transaction, Lazy Load, Cache, Log等少量样板应用之外,AOP的技术价值究竟何在? 它能否在广泛的领域发挥作用? 为什么考虑到传统领域之外的应用时,我们的想象力是如此的贫乏?回答这些问题需要对AOP的技术实质作详细的审视. 传统上, 程序的结构是静态的. 定义了一个类, 它的成员变量和成员函数就是确定的了,定义了一个函数, 它的具体实现也是确定的. 传统程序设计主要定义了一些固化的规则来规范这些确定性组分的组合关系,如类继承体系所表达的推理关系. 而AOP是一种动态代码织入技术, 抽象的说, 一维拓扑的基本元素是线段与边, 而AOP通过mixin, interceptor等机制可以自由的实现这些元素之间的自由组合而不拘泥于预制的规则. AOP就像是一把锋利的砍刀, 我们用它从最终所期望的程序结构中随意的砍下一部分来, 起个名字,就叫Aspect吧. 实际上AOP技术本身并没有限定程序中哪些部分可以作为Aspect, 这种技术本身并不保证你可以抽象得出真正有价值的Aspect, 它只是一种纯粹的程序结构操纵技术而已. AOP技术有两个主要组成部分: 定位技术和定位后的组装技术. 定位技术是AOP所宣称的无侵入性的关键所在. 如果我们使用interface等机制来实现功能,则要在程序各处写下调用语句: interfaceA.methodA(); ... interfaceA.methodB(); 这可以看作是一种占位技术. 定位技术则一般不需要预先在程序中写下什么调用语句, 根据外部的某些定位规则,我们可以在基础的程序结构中搜索到适当的位置. 在理论上说,这种定位方式非常灵活, 即可以是非常精准的定位到某个点,也可以是非常宽泛的定位到一组切入点. 但是, 这里的一个隐含假设是程序基础结构本身已经具备了良好的,具有某种均一性的坐标系, 只有这样我们才能够拥有定位所需的基本信息. 想象一下,如果整个程序只有一个函数, 所有功能的实现通过传入不同的参数值来实现, 则这样的程序结构中是没有什么可定位性而言的. 早期AOP定位所能够依赖的坐标只有类,方法名称, 方法参数类型等, 而这些信息本身又具有自己的业务含义,随着业务的发展,它们本身的名称也可能需要不断的变化,这直接造成AOP所依赖的坐标系的不稳定性.今天还有效的位置描述, 明天也许就突然包括了某些不应该包含进来的程序位置或者排除了某些应在其中的位置. 在JDK5.0中补充的annotation机制为程序补充了新的坐标维度, 基于它无疑可以建立更加灵活而且专用的坐标系统, 它对于AOP的价值必然会逐渐被发掘出来. 在javascript这样的动态语言中,虽然它们内置的动态性直接支持程序结构的动态组装, 但是在定位支持方面却要比java这样的语言弱上很多, 在其上建立AOP应用未见得比java更具优势. 从另外一个角度上说, 定位方式也并不总比占位方式优越. 我们需要牢牢记住"一次描述"的优势在于可以"多次应用". 有的时候我们用很多唇舌去描述一个物品看起来像什么什么样, 有多么大, 多么重, 还不如直接拿给人看, 说:嘿, 就是这个(this).同样在程序中, 占位方式可能更加直接简单,甚至因为代码明确写在那里,概念也更加明确,更加完整. 此外, 在一些特定的程序结构中, 需要定位的位置大大减少, 我们也不需要复杂的定位机制. 例如在witrix的jsplet框架中, 因为它特殊的规范一致性造成程序的处理点只有一个, 应用AOP是一个非常直接的过程. AOP的第二个组成部分是组装技术. 程序结构的组装在java中早已不是什么技术难点, 很多人干起这活来都是轻车熟路. 但是组装不仅仅意味着程序结构的融合, 它同时意味着程序运行时状态空间的融合. 在AOP的基础模型中, 在切入点可以得到的变量有this指针,调用函数对象和传入参数列表, 但是AOP本身并没有进一步规范化这些变量的具体形式, 因此在一般情况下, 这些变量对于interceptor来说是只读的, interceptor之间也无法通过这些变量交换信息并协同运行. 这实际上意味着在切点处inteceptor的状态空间是极端受限的. 而当一个切面横跨很多切点的时候, 在interceptor中一般只能对切点处状态空间的共性部分进行操作, 这进一步限制了interceptor的能力.实际上目前AOP的主要应用都是无状态的,或者是基于全局状态空间的(例如transaction interceptor只访问线程上下文), 这反映出对于AOP的primitive方式的应用的一种局限性. 在witrix平台的BizFlow设计中,通过对状态空间明确建模, 实现了基于AOP概念的一种新的应用方式. AOP需要对程序坐标空间和状态空间的良好规划, 才能保证无缝的织入, 保证信息交互通道的通畅.应用AOP所指的不仅仅是配置使用现有的AOP实现, 基于这种程序结构操控技术我们所需要做的抽象工作还很多.
实现是一种具体的东西,对于同一种实现可以有不同层面的各异的诠释,而每一种诠释都可能对应于不同的概念体系。对于一种具体的技术实现,总有人争论说这样这样一下,不是也可以做到什么什么效果吗。但这只是实现上在某种程度上可以达到目标,并不意味着该实现基于的概念体系自身直接体现了我们所需要的结构。这就如同我们可以用汇编实现一切,但并不意味着汇编语言本身的结构包含了所有高级程序结构一样。同样,说到一种失败的技术的时候,也需要区分清楚问题是出在实现细节上还是对应的概念体系上。
按照Tim Berners-Lee的原始设想,互联网的核心概念是超链接(hyperlink), 每一个可访问的资源都具有自己的URI(Universal Resource Identifier), 我们通过唯一的url可以访问到这些资源。从概念上说,每一个页面可以由一个两元组[title, url]来进行描述,title是url显示在界面上时的可读表述。在这一描述下,我们可以建立基本的页面浏览模型,包括浏览历史模型:我们可以把浏览过的页面保存在浏览历史列表中,通过选择历史列表中的条目实现页面跳转。但是随着网页的动态性不断增加,页面的资源语义逐渐丧失,url所对应的不再是一种静态的资源表述概念,而逐渐演变成为一种动态的访问函数. 例如 /view.jsp?objectName=MyObj&objectEvent=ViewDetail&id=1 一个url模式所对应的网页个数在理论上可能是无限多的. 从单一数据值到函数是系统复杂性本质上的一种飞跃. 为了不致在这种无限的世界中迷失方向,我们显然需要对于浏览过程进行更加有效的组织,建立更加有约束性的导航模型. 一种常见的导航模式是在页面的上方显示一条线性的导航路径, 与浏览历史模型不同的是, 页面转换时不是 list > view item 1 ==> list > view item 1 > view item 2 而是 list > view item1 ==> list > view item2 为了支持导航路径, 最简单的方式是将页面模型扩展为三元组[id, title, urlExpr], 其中urlExpr是动态表达式, 而id是固定的值. 例如 id=view, urlExpr=/view.jsp?objectName=MyObj&objectEvent=ViewDetail&id=${id} 导航路径的变化规则为navHistory.removeAfter(newPage.id).add(newPage) 为了进一步约化导航路径, 可以将页面模型扩展为 [id, title, urlExpr, group, before, beforeGroup], 其中group定义了页面的分组, 同组的页面在导航路径中相互替代, 而before和beforeGroup定义了页面在导航路径中的相对顺序. 例如对于 [id='list' beforeGroup="detail"] [id='view' group='detail'] [id='update' group='detail'] 在页面转换时, 导航路径变化不是 list > view item1 ==> list > view item1 > update item1 而是 list > view item1 ==> list > update item1 在以上的页面模型中, 每一个id对应的是一个不同的页面模板(页面布局), 但是有时我们也需要在同一个页面布局中浏览无限分级的栏目, 此时可以将页面模型扩展为 [id, title, urlExpr, group, before, beforeGroup, path] 将以上的导航模型应用于一般的web应用时还需要克服一个小小的困难: 动态url的副作用. 例如/update.do?id=1&value=2这种具有动作语义的url访问直接破坏了页面的浏览模型,例如现在我们不再能够按F5键刷新页面,不能通过window.location=window.location调用来刷新页面,在页面回退时我们也将遇到种种困难。为了防止重复提交,一种常见的设计模式是分离POST和GET方法,即通过Form POST向上提交数据,处理完毕后通过redirect指令通知浏览器再次发起GET方法得到新的页面.具体做法如下 /redirect_after_action.do?id=1&value=2 在执行完action之后, 服务器调用response.sendRedirect(get_url), 通知前台浏览器使用新的url重新取回页面. 这样最终我们可以确保所有页面都是可以通过url直接访问的(没有隐含的post参数),而且是可以重复访问的(无副作用因而可以反复刷新)! 另一种方式是使用ajax方式提交更新请求,当ajax访问成功后, 在回调函数中进行页面跳转.例如: new js.Ajax().setObjectName('Test') .setObjectEvent('Update').addForm(myForm) .callRemote(function(returnValue){ window.location = '/view.jsp?id=1'; }) 这里我们也可以根据returnValue的不同选择跳转到不同页面. 在Witrix平台中, 基于Jsplet框架我们定义了PageFlow对象, 它将可配置的导航模型引入到任意页面的组织过程中. 在跳转到一个新的页面的时候, 访问url如下: /pageflow.jsp?objectName=MyNav&objectEvent=NavToPage&_pageId=view&id=3 在重新访问历史页面的时候,只需要 /pageflow.jsp?objectName=MyNav&objectEvent=NavToHistoryPage&_pageId=view 基于jsplet框架的对象化特性,MyNav对象在session中保持了flow的所有状态变量,不需要任何框架上特殊的支持。我们可以在任意页面跳出pageflow, 并在任何时刻选择跳回pageflow, 这些动作不会影响到flow对象的状态。而通过objectScope的设置我们可以精细的控制flow对象的生命周期。同时基于对象化设置,访问页面时我们只需要资源的相对名称(relative identifier), 例如对于各种不同的flow, 页面id都可以叫做view, 通过_pageId=view来访问。 Apache软件基金会旗下有一个beehive项目, http://beehive.apache.org/docs/1.0m1/pageflow/pageflow_overview.html, 其中也定义了所谓pageflow的概念, 这是始创于BEA的一项技术. 但是与Witrix Page Flow不同的是, Beehive Page Flow的核心概念仍然是从struts引申而出的action的概念,所谓的flow是action的连接,而不是通过资源id之间的连接。虽然beeive宣称支持各种对象injection, 并且管理了flow对象的生命周期,但是它并没有规范出this指针这一面向对象最重要的概念之一。Witrix Page Flow关注的重点是已存在的页面之间的有效组织, 它所描述的是某种具有静态资源语义的页面模板之间的关联, 这种关联是较松散的,而不是通过action强关联的. 实际上基于Ajax技术和jsplet框架的消息路由机制, 在每一个页面上都可以进行多种业务操作,甚至更换页面,而不发生pageId的变动, 并不是所有的操作过程都需要在page flow描述中对外暴露。另外一个比较细节化的不同之处是Beehive使用Java Annotation, 而Witrix PageFlow使用xml描述文件, 并实现了pageflow的继承和部分覆盖等高级构造方法. 使用xml描述文件的必要性在于需要复用WebAction, 支持复杂的变换操作, 并集成tpl模板引擎等. Annotation的方式看似简单,但这种简单性同时将系统中的所有组分绑定到了一起, 它也不允许复杂的变换操作的存在. 实际上Beehive的目标并不是真正支持页面编制(包括页面上的页面操作)和页面组织的分离.
IoC(Inversion of Control)是一个很宽泛的概念,对于我们常说的IoC容器(如spring)所做的工作,一个更加准确一些的说法是依赖注入(Dependency Injection), 即容器将一个对象所依赖的其他对象push到该对象中,而不是该对象从外界环境pull相关的依赖对象. 不过, 细究起来这种依赖注入仍然只是DI的一种特殊形式, 可以将它称之为data dependency injection, 因为IoC容器所许诺的是: "啊哈, 当我们需要对象A的时候,它就在这儿". 虽然IoC容器管理的不仅仅是数据,而是具有行为的对象,但是如果要让这些行为具体发生, 我们仍然需要额外的调用步骤. 对于一个自动触发behaviour的系统, 我们一般将之称之为引擎(Engine). 例如工作流引擎在处理完本步骤的业务逻辑之后会自动触发流程流转操作. 一个引擎对于我们的承诺是: "当我们需要某个behaviour的时候, OK, 它会在适当的时候发生的". 对于一个软件开发框架或者更宏大的软件开发平台而言, 如果我们以业务逻辑为主体来审视整个程序运行过程, 则它们的核心价值也在于在适当的时候将适当的操作Inject到业务逻辑中. 对于目前所谓的软件开发平台而言, 除了工作流的内容之外, 一个主要部分就是CRUD(Create/Read/Update/Delete) Ready. 一个开发平台的优劣往往直接体现在它在多大程度上能够将CRUD操作剥离出主体程序逻辑, 这不仅仅涉及到数据库存取操作, 同时还包含界面交互, 数据逻辑关联等. 除了引擎,框架,平台等应用层面的实现之外, AOP(Aspect Oriented Programming)在语言层面为behaviour注入也提供了一种特定的实施手段. 在AOP的概念中, 往往作为切点的函数被认为是基础的部分, 而interceptor是在基础蓝图上的一种扩展. 这也体现在基础函数定义了当时执行环境中可以访问的状态变量(参数/属性),而interceptor则依附于pointcut处所能得到的状态变量, 它本身一般并不维护独立的状态变量(不产生也不消灭状态变量). 从数学上看, base function和interceptor之间构成一种对偶(dual)关系, 当我们的关注重点转移到interceptor上来之后, 它本身也应该具有完整的业务语义, 这需要对AOP的执行过程做小小的偏置处理. 在witrix平台的BizFlow设计中,BizFlow是通过类似AOP的方法作为CRUD操作的Filter实现的, 但是从bizflow本身的配置文件来看,它可以构成一个完整的业务描述, 而CRUD成为某种自动注入的behaviour.例如对于新增操作, BizFlow中的配置可以如下 <action id="Add-default"> </action> 虽然没有写任何代码, 新增操作(包括从request中读取操作并做有效性校验等操作)是自动进行的. 我们也可以在新增前和新增后分别执行一些操作 <action id="Add-default"> <source> some operation before add <biz:Proceed/> some operation after add </source> </action> 与AOP中的常见做法不同, 这里并没有明确定义新增前和新增后这样的切点, 而只是定义了Add-default这样一个action. 在BizAction的source段可以执行任何tpl代码, 而tpl代码的执行上下文可以看作一个Map, tpl代码执行过程中可以获取变量, 也可以设置任意变量, 因而bizFlow拥有对于状态空间的完全的控制权.
对于目前MDA(Model Driven Architecture)的理论和实现,我一直持一种消极态度。以前和hotman_x的讨论中,我也明确表述过对于MDA的看法。 MDA:以有限搏无限 http://canonical.blogdriver.com/canonical/787637.html 图形 vs. 文本 http://canonical.blogdriver.com/canonical/1090209.html 所谓的MDA一般总是从高层抽象模型出发,希望通过预定的建模过程推导出底层的全部实现细节。但是implementation is also interpretation. 实现过程本身也是对高层模型的一种诠释过程, 是一个逐步明晰并逐渐消除概念之间矛盾冲突的过程。从高层模型到底层实现并不是一个同构(isomorphism)的过程,甚至一般情况下也不是同态(homomorphism)的。在概念模型到具体代码实现的过程中,总是存在着需要不断补充的细节。这些细节如何才能成为高层模型的一种自然的衍生部分是一个非常复杂的问题。如果考虑得太细(需要指定过多难以从整体上进行控制的参数),似乎就会丧失高层模型的抽象性和概括性,而如果不深入到细节,则难以平衡高层模型之间的互相冲突的属性。随着细节的不断增加,试图维持高层模型在各个层面的统一性无疑将变得异常困难。实际上在每一个抽象层面概念都可能出现重组和混合的情况,试图统一建模在目前的技术水平下是不太现实的。 MDA所需要解决的一个核心问题是维护模型的持续有效性, 即当根据模型构造出实际系统之后, 对模型的修改仍可以自动反映到已实现的系统中, 而不是每次重新生成一个新的系统. 或者说MDA应当如何支持实现层面的重构. 为了解决这个问题, 一般的实现策略是建立完整的程序模型, 提供一个强大的集成开发工具, 可以在一个特意构造出的IDE环境中对模型进行调试, 修正, 尽量避免程序员直接接触实现代码, 确保一切细节尽在单一开发工具(单一信息驱动源)的掌握之中. 但是很显然, 这样一个大一统的开发工具在各个层面(如数据库设计, 表单设计等)都只能是专业工具的一个简化版. too simple, sometimes naive. 当我们需要对程序有较深入的控制力的时候, 这些工具往往就很难起什么作用了, 甚至会成为某种障碍. 在witrix平台的设计中, 也有部分"MDA"的内容. 只是我们的设计思想是Physical Model Driven(物理模型驱动), 而不是Logical Model Driven(逻辑模型驱动). 具体做法是 1. 采用power designer建立数据库物理模型(PDM 而不是 CDM), 然后根据一些命名约定和附加注释(例如pdm中的package映射为java实体类的package, pdm的domain指定字段是否附件字段等)来标注出物理元素的逻辑含义. 2. 解析pdm文件, 生成hibernate映射文件(.hbm.xml), meta文件(.meta.xml), spring注册文件(.action.xml)等 3. 通过jboss的hibernate-tools工具生成java实体类. 根据自动生成的配置文件, 可以直接完成对于数据库的增删改查操作, 包括维护一对多,多对多等关联关系. 此后我们可以根据程序具体需求, 对生成的文件进行修改. 通过一些程序设计技巧, 我们可以实现手工修改的代码与工具自动生成的代码之间始终有明确的边界, 从而可以做到pdm与代码的自动同步, 不需要手工进行任何调整. 我们选择从PDM出发的一个基本理由在于, 从高层模型向下, 路径是不确定的,而从物理模型向上,路径是确定的. 在pdm中我们需要做的不是补充细节(增加新息)而是标注出已经存在的逻辑概念。选择PDM建模也集中体现了我所一直倡导的Partial Model的概念. PDM模型并不包含界面的具体展现方式, 也不包含更加复杂的业务处理过程, 它只是完整程序模型的一部分. 我们不试图建立一种完全的模型,追求概念上的一种自我完备性, 而只是关注当某些被共享的模型元素发生变化的时候, 这些变化如何以保真的方式传播到系统各个角落. 实际上一旦获得物理实现,高层模型在某种意义上就变得不再那么重要了. 为了支持模型的局部修改, 我们只需要从物理模型中提取部分信息,而不需要恢复出一个完整的业务模型。我们不需要把一个高层概念在各个层面的表达都考虑清楚,我们只需要知道某一特定的物理模型应该对应的部分高层模型即可。 目前国内一些软件开发平台也包含所谓MDA的部分, 例如浪潮Loushang MDA ( www.loushang.com)号称不需要写一行代码,定制出所需应用系统. 可从其技术白皮书来看, 它所谓的Model虽然是使用UML来建立, 但是大家似乎故意忘记了对象是成员变量+行为构成,不包含行为模型的对象模型不过是数据模型的一种翻版而已. 从Loushang MDA的元模型对象的UML图可以看出, MofTab, MofReference等固定了几种界面显示模式, 似乎其MDA只是针对既定场景应用的一种预制代码框架. 从我们的实践来说, 数据模型驱动的应用并不需要限制在基础数据对象维护这一非常特定的领域,而可以在通用应用领域发挥作用。
程序中的各种类(Class),包(package)等首先体现的是架构设计中的一种概念分布. 一个良好的设计相当于是建立一个结构合理的概念框架, 随着系统的不断发展, 作为概念载体的类(Class)不断吸收相关的实现, 从而使其外延不断丰富起来, 而其内涵也愈加变得明晰. 系统中概念的分化, 最显著的不是业务模块的划分, 而是技术层面与业务层面的分离. 因为技术手段与业务在很大程度上是相互独立的, 因为 [无论]实现什么样的业务, 我们[都]将用到某种技术手段. 而当我们可以回答一个"无论..都" 的问题的时候, 它意味着某个概念可以容纳众多变化, 而它自然有资格成为某种独立的部分. 作为技术层面概念聚集的例子, 我们可以看一下spring framework中的JdbcTemplate类, 这个类在spring的概念体系中对应于"Jdbc调用帮助类"这一概念, 它的目的是帮助我们尽量通过一次函数调用得到我们所要的结果, 但是我已经不止一次的看到很多人使用如下调用 List results = jdbcTemplate.query(...); List ret = new ArrayList(); for(int i=0;i<results.size();i++){ ret.add(((Map)results.get(i)).get("someField")); } 这段代码的目的是为了得到某一列的值, 而JdbcTemplate类没有直接提供这一函数. 为了不等待spring的升级, 显然我们需要建立一个JdbcTemplate的扩展类, 它直接提供一个queryScalarList函数, 而不是让这种纯粹技术性的循环语句散见在程序代码的各个角落. 告别裸奔编程是我对同事的基本要求之一. 即使是考虑最细致的软件组件, 它也难以保证能够预想到所有的变化形式, 而在系统中集成一些第三方组件的时候, 一般总要加入一些特定的假设, 此时也需要一个技术隔离层. 例如在页面开发中, 我们强制使用witrix平台定义的js.Ajax对象, 而不是prototype.js中原始提供的Ajax.Updater等对象. 在应用一段时间之后, js.Ajax对象上聚集了一系列与ajax相关的调用指令.
hibernate的应用中一般总是将entity映射为强类型的java类,这为程序操纵带来很多便利,同时可以将大量动态过程隐蔽在对象包络之下。映射为java类的一个主要问题在于无法在程序运行时刻对于程序进行修改,而数据结构的局部修改几乎是无法避免的。hibernate3本身支持动态数据模型,它允许我们把entity映射为Map数据类型, 当数据结构发生变化的时候, 只需要修改hbm文件即可改变映射模型,而不需要修改java实体类代码. 在hbm定义文件中,如果我们不指定name属性,而是指定entity-name属性,则我们可以将entity映射为Map, 而不是一个java实体类. <class entity-name="test.DynamicEntity" table="DYNAMIC_ENTITY" >...</class> 此外, 也可以选择将部分字段动态映射到Map <class ...> <dynamic-component name="dynamicAttributes"> <property name="foo" column="FOO"/> <property name="bar" column="BAR"/> </dynamic-component> </class> 在HQL语句中可以直接使用o.dynamicAttributes.foo来访问foo属性,所有操作与普通属性相同。 为了实现hiberante映射模型的动态更新,我们首先需要实现sessionFactory的动态更新。目前hibernate的实现只允许从hbm文件重建sessionFactory, 即新建一个sessionFactory替换原有的sessionFactory, 在使用spring的情况下,这需要对org.springframework.orm.hibernate3.LocalSessionFactoryBean进行小小的hack。 为了将动态属性和普通属性同样对待,要求在操作实体对象属性的时候需要能够自动处理nested property, 即需要如下类似的方法:entityDao.getProperty(entity,"dynamicAttributes.foo"), entityDao.setProperty(entity,"dynamicAttributes.foo", attrValue). 为了使得应用程序自动处理新增属性,要求程序是meta驱动的:当实体对象增加了一个属性时,只需要修改meta文件,即可完成对于该属性的增删改查汇总等普通应用需求。
最近ruby语言的流行似乎再次引发了DSL(Domain Specific Language)讨论的热潮。从语法表现形式上看,通过对于ruby语言的深度hack, 充分挖掘ruby语言的某些语法特征,可以使得正常的ruby语句看起来比其他计算机语言更接近于人类的自然语言,某些人因此认定ruby语言是DSL的天然载体。但是在我看来,具体语言的语法表达形式对于DSL的核心价值而言并不是最关键的。 首先,DSL的核心在于高效的表达语义,而并不在于是否接近自然语言。接近于自然语言并不意味着更加domain, 因为自然语言也是一种通用语言,它未必能够比采用其他语法形式的语言更加有效的对domain事物进行描述。典型的有数学符号和化学分子式。 第二,作为DSL, 紧凑的表达形式是一方面,另一方面是这种表达形式的稳定性,即如何防止人们写出不符合DSL规范的语句。ruby语言的片断直接作为DSL无疑是一种naive的解决方案,我们可以轻易写出大量不同形式的ruby语句,而它们在语义上是等价的(这意味着通过单元测试也无法发现它们的不同),即人们不按照设计的DSL语法书写,这造成DSL的解体。 作为一种DSL构造语言,其核心能力在于如何将second class的domain中的概念(非语言本身内置的概念)封装到first class的表达形式中。ruby作为一种动态语言,可以更加轻易对于自身meta data进行内省,典型的如ruby中的ActiveRecord设计. 但是在我看来,这种概念提升能力在ruby的语法结构中也是有限的,原因恰在于ruby的语法太多样化了。实际上,我更加看好xml结构的均一性。
CRUD(Create Read Update Delete)是一般应用程序中最基础的操作,但是用户的需求却很难直接映射到CRUD操作上。例如常见的需求如下: 1. 不同的业务处理处于不同状态的业务对象: 业务A处理状态为X的业务对象,而业务B处理状态为Y的业务对象 2. 业务对象处于不同状态的时候允许的操作不同: 状态处于X的业务对象允许操作U, 而状态处于Y的业务对象允许操作V 3. 不同的业务操作可能修改业务对象的不同属性: 操作U修改业务对象的属性P, 操作V修改业务对象的属性Q 4. 具有不同权限的人能够从事的业务不同: 角色R处理业务A, 角色S处理业务B 5. 具有不同权限的人即使从事同一业务,所能够操作的业务对象集合也不同: 角色R处理部门M的业务对象,角色S处理部门N的业务对象. 6. 具有不同权限的人即使可以操作同一业务对象,所能够采取的业务操作也不同: 角色R只能进行操作U, 角色S只能进行操作V 7. 在业务对象上执行操作之后可能造成状态变迁: 处于状态X的业务对象上执行操作U后状态变为Y
以上这些需求往往是系统中最易变的部分, 而它们在概念上恰恰表现为对CRUD的一种限制性描述. 因此通过如下扩展我们可以定义BizFlow的概念: BizFlow = CRUD + Filter. 根据这种观念, witrix平台中BizFlow被实现为DaoWebAction的一种无缝扩展. 在jsplet框架中我们通过如下url模式来访问后台的CRUD操作: /list.jsp?objectName=MyObj&objectEvent=Query 为了实现BizFlow只需通过spring为DaoWebAction配置一个xml配置文件, 此后仍然可以通过 /list.jsp?objectName=MyObj&objectEvent=Query 来访问后台的CRUD操作,只是后台会自动应用配置文件中的 bizId="default", bizActionId="Query-default"等配置项. 如果我们采用如下url来访问 /list.jsp?objectName=MyObj&objectEvent=Query&$bizId=test&$bizActionId=test 则后台将应用配置项 bizId=manage, bizActionId=Query-test, 而 /list.jsp?objectName=MyObj&objectEvent=BizAction&$bizId=test&$bizActionId=test 则对应于配置项 bizId=manage, bizActionId=BizAction-test. 应用BizFlow配置项之后,所有前台代码都可以不做出任何改变, 因为它们只是对于给定数据的展现. BizFlow可以看作是CRUD加上简单的流程控制和权限控制所构成, 但是它与完整的工作流模型还是有着显著区别的. 工作流中所关注的重点首先是流程实例而不是业务对象实例, 在一个流程中是否存在唯一的业务对象,以及业务对象的状态是否随着流程流转发生变化完全是一件独立的事情,它们并不属于抽象的工作流模型本身. 理论上说,一个业务对象可以同时参与多个流程. 在工作流建模中主要通过流程步骤的先后顺序的约束来描述业务进程, 处于同一状态的业务对象可能处在不同的流程步骤中. 而BizFlow可以看作是状态驱动的, 当前业务步骤直接由业务对象的状态决定. 在BizFlow中因为视角是业务对象的状态,因此我们直接面对的是大量处于同一状态的不同的业务处理过程, 而workflow中往往建模的时候强调单流程实例视角,而一般缺乏对于流程实例相关性的描述. 现在国内很多人认为工作流就是状态机其实是对workflow概念的一种误读.
在国内做项目,客户需求控制总是个令人头痛的问题。很多时候问题在于用户并不知道确定自己需要什么,他只是就着目前系统的状况提出一些他可以设想到的情形, 但最终他可能并不真的使用这些功能,或者他的想法会发生其他的变化。所以如何引导客户走到我们所谓的"正确"方向上来是一个非常重要而复杂的问题。 昨天一个同事就此事做了个有趣的比喻。客户的需求是吃饱饭,你给他一个馒头,他和你争论馒头是大点好还是小点好。但是如果你给客户一个包子,情况就完全不同了,客户关注的重点肯定是包子是什么馅,猪肉三鲜还是韭菜鸡蛋。。。很多人还有忌口的。从此没完没了。
http://code.google.com/webtoolkit/ 最近google发布了Google Web Toolkit(GWT)开发包,这是一种使用java语言开发AJAX应用的开发框架。从技术上看,GWT并没有什么新鲜之处,类似的概念在多年之前就已经有各种尝试了,这些尝试从未真正吸引到足够的注意。GWT的优势也许在于提供了一套模拟工具,另外可能在屏蔽browser的兼容性和bug方面做得更好一些,但是真正的技术思想并没有什么突破. Ruby On Rails同样是试图将ruby语言直接映射到前台程序, 但是它通过一个通用的prototype.js库最小化了ruby语言和js语言之间的区别,在概念上要比GWT的java2js的compiler概念要更加新颖一些. ( http://mir.aculo.us/stuff/COR_20060413_RailsAjax.pdf) 对于web开发而言,我总认为要发挥web的特色,而不是把它约束到其他领域的开发模式上。js+dom+html文本所能做到的结构控制程度要远远超越组件技术,我也从未发现学习java要比学习html要更加容易。也许对于某些对于web一无所知的java开发人员来说,GWT有些意义,也许GWT会特别适合于某些特定的领域,但是作为一种通用的开发框架,我并不看好它。
|