继承与混合,略谈系统的构建方式

这两天在读kissy的源代码,从一开始我就对它的mix()函数充满了敌意。因为无论从哪个角度来看,那都是一个极其低效的实现。不过深入了解这个框架之后,我对kissy中的新的系统构建的模型产生了兴趣,而这种系统构建的方式,也正是由mix()所带来的。

一、对象系统

我们先了解一下对象系统。在《JavaScript语言精髓与编程实践》中谈到过,面向对象系统有三种对象的继承方式,即原型、类和元类。这三种方式都可以构建大型对象系统。在后续讨论之前,我们在名词概念上做一些强调,所谓“对象系统”,是指由“一组对象构成的系统”,这些对象之间存在或不存在某种联系,但通过一些规则组织起来。所谓“面向对象系统”,是指以上述“对象系统”对基础延伸演化的系统,新系统满足前对象系统的组织规则

所谓对象系统的三个要素,即继承、封装与多态,即是上述组织规则的要件。孟岩同学从C/C++出发,从另一个侧面谈论对象系统,所持的观点我相当认可,这包括所述的“对象范式的基本观念中不包括继承、封装与多态”——这一观点有其确切的背景与思考方法,值得一谈。

参见:孟岩《function/bind的救赎(上)》

我们在这里要讨论的是“对象系统”,即,对象是如何组织起来的问题。在这个问题上,组织规则之一,就是“继承”。JavaScript中基本的继承模型是原型继承,其特点是“新对象实例的特性,复制自一个原型对象实例”。Qomo以及其它的一些项目,通过语言扩展的方式,在JavaScript上添加了类继承的模型,其特点是“对象构建自类,类是其父类的一个派生”,这里的“派生”与“特性复制”有潜在的关系,即:子类的特性也复制自父类。正是因为“派生”其实是特性复制的一种形式,所以事实上Qomo中的类继承,是通过原型继承来实现的,因为原型继承本质上也就是“特性复制”。

无论是原型继承、类继承还是这里没有进一步讨论的元类继承,继承的最终目的是构建一个“对象系统”,而不是“系统”。这一个小小的措辞上的区别,有着本质上的、深刻的意义,这也是我提及到孟岩的上一篇文章的原因。通常,由“继承”入手理解的“对象系统”其实是静态的,以至于我们面向对象系统开发的最后一步,仍然需要框架来驱动之。例如TApp.Run(),或者类似于new App(),等等。继承所带来的,主要仍然是指对象系统的组织性,而非其运行过程中的动态特性。于是我们通过更多类或其它对象系统,来将一个系统的动态特性静态化,例如将对象之间的交互关系抽取出来,变成控制类。我们做这些事情的目的,仅仅是因为我们约定了对象系统的组织规则,要面向这个对象系统开发,也必然满足(或契合)这一组织规则。组织规则限定了我们构建系统的方式——继承、封装与多态,这在一定程度上说是“对象系统构建”的一个方案,并非“系统构建”的方案。而孟岩在上文中讨论的,也正是“系统构建”的问题。所以孟岩提出两点:

  • 程序是由对象组成的;
  • 对象之间互相发送消息,协作完成任务。

其第一条,是对象系统的基本特性,是谓系统成员;第二条,是对象系统如何演进为系统的特性,是谓系统通讯。一个系统的约束,既包括其成员(以及成员的组织规则),也包括成员间的通讯。

二、用mix()来构建系统

舍弃“继承”这种方式不谈,系统构建还有其它的什么方法吗?

kissy提供了另外一种可能性,即mix(),混合。在kissy系统的核心部分,为一个系统提出了三个概念:

  1. 原子(meta),一个系统具有至少一个原子,原子是具有mix()能力的一个对象;
  2. 宿主(host),一个系统有一个依赖的宿主,表明系统的外部环境,系统只是其宿主环境中的部分内容,可以由特定的名称来区别于其它;
  3. 种子(seed),一个系统诞生自一个种子,种子描述系统上述的meta和host两个方面的特性。

kissy约定,一个系统诞生自一个种子,该种子通过不停地mix()而成长,变成一个复杂的系统。由种子培育成为系统的整个环境,只需要能够理解mix与host,即可以基于seed来构建任意复杂的系统

上述的逻辑在kissy.js中,描述得相当简单:

(function(host, S) {  
  
    var meta = {  
        mix: function(r, s, ov, wl) {  
             ...  
        }  
    },  
  
    // If KISSY is already defined, the existing KISSY object will not  
    // be overwritten so that defined namespaces are preserved.  
    seed = (host && host[S]) || {},  
  
    // The host of runtime environment. specify by user's seed or <this>,  
    // compatibled for  '<this> is null' in unknown engine.  
    host = seed.__HOST || (seed.__HOST = host || {});  
  
    // shortcut and meta for seed.  
    S = host[S] = meta.mix(seed, meta, false);  
  
    return S;  
  
})(this, 'KISSY');

这个系统初始化的时候,传入host与host中的系统名S。对于kissy来说,host是当前的系统环境,这里的this值,可以是javascript引擎的global,或浏览器环境的window,或某个函数或对象闭包内的当前this。而'KISSY'值,表现kissy系统在环境中的名字。按照javascript的语言约定,我们可以通过host[S]来找到既已经存在的kissy系统。

按照此前的约定,一个mix构建的系统,必然有host和mix两个性质,因为它最原始的种子(seed)就必然包括这两种性质。所以,既然我们上面是通过host[S]来访问一个“既已存在的kissy系统”,则无论该kissy系统经过了何种程度的演化,必然会包括这两种性质。

上面的构建过程尝试寻找在host[S]中寻找这两种性质,如果其中之一不存在,则尝试初始化它。例如代码:

seed = (host && host[S]) || {},

如果host[S]是存的,则假设它是一个seed,否则初始化为一个空的对象。接下来:

host = seed.__HOST || (seed.__HOST = host || {})

如果上述的种子seed有host属性,则使用它既有的__HOST,如果没有,则置为当前环境下的host,或一个空的对象。

现在我们看到的seed,必然已经具有host属性。但是,它还“可能”缺少一个性质,即最最重要的mix()。mix()的作用其实很简单,就是从对象B将属性抄写到对象A的一个方法。而这里,之所以说是“可能”缺少,是因为如果seed是既有的mix系统,则他已经有mix()属性;如果它是第三方系统,则可能没有mix,或有一个不同的mix等等。下面的一行代码尝试用元语言的思想构建它,即:

meta.mix(seed, meta, false); // false值表明不覆盖

元语言的特点是自描述的,meta.mix()可以向seed混入mix(),也可以使seed.mix()能混入其它系统或meta本身。总之在mix()的构建中,meta只需要有mix这个方法,不需要更多,也不能更少。

上一行代码的结果,是:如果seed没有自已的mix()属性,则向seed混入meta的原始的mix()。

现在,我们再看seed,必然已经具有了host和mix()属性。它本身可能是一个空对象,也可能是一个庞大的既有系统,但无论如此,它具有了这两个性质,就可以作为seed进一步的衍生。

在这一切之前,下面的代码保证它位于HOST[S],并返回这个系统:

    S = host[S] = meta.mix(seed, meta, false);  
    return S;</textarea>   

三、mix()系统构建中的其它概念

kissy除了实现基本的mix系统之外,在core部分加入了其它的一些功能。包括除mix()之外的两种混入方法:

  • **augment,扩充。**用mix方法,将另一些子系统s[i]的原型,混入目标子系统r的原型。
  • **merge,合并。**用mix方法,将另一些子系统s[i],混入当前子系统S。

基本上来说,augment是通过mix来对javascript的原型系统进行扩充的方法,或是在应用系统中,结合原型机制与混入机制来构建系统。而merge只是mix方法的一个批量工具。

另外,考虑到面向对象系统中的继承特性,kissy也实现了extend(派生)方法,以提供传统的面向对象编程能力。

除了语言级别的概念之外,kissy也提供系统框架级别的一些构建能力。包括:

  • **app,应用。**与host[S]并列的,具有同等能力的其它应用,app('XXX', ...)可以在host['XXX']上组织应用。
  • **namespace,命名空间。**即可以组织出host[XXX].YYY.ZZZ这样的,在不同子系统中的,不同命名空间下的系统。

最后,kissy在内核中也提供简单的调试支持。

显然的,基于mix的原则,任何一个第三方的系统可以通过混入kissy来修改上述的概念,例如覆盖extend()来实现自己的对象系统构建原则,或覆盖app()来实现自己的应用组织原则。第三方系统也可以将kissy混入自身,在保障自身特性的情况下,使用kissy,以及更大规模的kissy ui系统带来的好处。

四、一点点提示

KISSY是什么?

KISSY是一个开源的javascript项目,其主体是一个前端UI开发框架,即KissyUI。本文所述的kissy是仅指其内核部分的kissy.js中的语言与框架设计思想。KISSY项目的开源网站是:http://kissyteam.github.com/

kissy怎么使用呢?

尽管在KissyUI向kissy内核化的过程中,我们提出了一些新的概念与框架模型,但事实上,我们并未改变KissyUI的任何使用惯例。从代码上来看,kissy.js和lang.js以后的其它模块,并没有任何的变化,因此如果仅是将kissy当成一个UI系统来使用,你可以参考上面的开源网站,其中既有的KissyUI文档是完全有效的,而且KissyUI本身也是一个优秀的、便捷的Web UI框架。但是,kissy系统在模向合并和组织上的能力大大增强了。例如说,我们可以开始想象下面这样的代码:

<!-- 先装载jQuery -->
<script src="jQuery.js" mce_src="jQuery.js"></script>

<!-- 将jQuery系统映射到KISSY,作为初始的seed -->
<script type="text/javascript">
  window.KISSY = jQuery;
</script>

<!-- 装载kissy,在jQuery上混入KISSY -->
<script src="kissy.js"></script>

<script type="text/javascript">
document.writeln('你现在使用的是Kissy,还是jQuyer?答案请选Y,或者Y'); 
document.writeln('你现在能装载Kissy UI,还是jQuery UI?答案请选Y,或者Y');

KISSY.merge(YUI, Dojo, Qomo).merge(Biby);
document.writeln("what's KISSY? select collapsar or black hole, pls.");
</script>