================================================================================
Qomolangma OpenProject v1.0
类别 :Rich Web Client
关键词 :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,
DOM,DTHML,CSS,JavaScript,JScript
项目发起:aimingoo ([email protected])
项目团队:../../Qomo_team.txt
有贡献者:JingYu([email protected])
================================================================================
一、Qomolangma中的接口(Interface.js)
~~~~~~~~~~~~~~~~~~
在做AOP(面向切面编程)系统之前,我一直在想:有什么必要在JavaScript中做“接口(Interface)”的
机制。——当然,这也说明,你可能需要通过阅读迟些提供的、关于AOP框架的文档,才能理解如何使用
Qomo中强大的接口机制。^.^
接口是现代软件工程中的一种常用工具,它的出现使设计人员更多的关注于功能的“对外表现”,而非
“内部实现”。在软件模型设计中,类图通常用于描述设计的细部,而接口则更常用于描述模块、层次
间的交互关系。
然而这仍然停留在“设计”层面。在C++中,你会看到“接口是抽象类”这样的描述。换个说法,C++认
为接口只是“没有实现的类”。如果这样来描述的话,JavaScript中就没有必要实现一个“接口机制”。
所以,我们看到altas中,接口的实现就简单得多。例如:
---------------
// 1. 声明接口
Web.IArray = function() {
this.get_length = Function.abstractMethod;
this.getItem = Function.abstractMethod;
}
// 2. 注册接口,以及为类注册接口
Type.registerInterface("Web.IArray");
Type.registerClass('Web.UI.Data.DataControl', Web.UI.Control, Web.IArray);
// 3. 接口操作(方法)
Function.prototype.implementsInterface = function(interfaceType) { ... }
Function.prototype.isImplementedBy = function(instance) { ... }
---------------
同样的原因,altas中的接口也仅在极少数的地方得以应用。例如在属性检测(类似反射)时,有这样的
代码:
---------------
Web.TypeDescriptor.getProperty = function(instance, propertyName, key) {
if (Web.ICustomTypeDescriptor.isImplementedBy(instance)) {
return instance.getProperty(propertyName, key);
}
// ...
}
---------------
我们看到atlas中的接口主要用于“检测一个类(或对象实例)”是否实现过某接口。这用于保障后续代
码能安全的执行。本质上,altas是用registerInterface()的机制,来使开发人员不必使用'in'运算来
检测属性。因而上面的代码事实上写成这样也没有关系:
---------------
Web.TypeDescriptor.getProperty = function(instance, propertyName, key) {
if ('getProperty' in instance) {
return instance.getProperty(propertyName, key);
}
// ...
}
---------------
——然而,这仅仅只是“接口机制”应用的一个方面而已。
在Qomo中,接口不单单是描述,也是实现。实现的接口(的方法)可以被调用,这与Win32中的COM机制
是类同的。Qomo在实现了更加完整的接口特性,并在这样的基础之上,完整地实现了AOP的种种特性。
与JSEnhance.js一样,Qomo中的Interface.js可以脱离Qomo项目运行。
二、概念:接口是描述,也是实现
~~~~~~~~~~~~~~~~~~
接口最基础的定义就是“描述对象的行为”。所以根本上来讲,下面的声明:
---------------
Web.IArray = function() {
this.get_length = Function.abstractMethod;
this.getItem = Function.abstractMethod;
}
Type.registerClass('Web.UI.Data.DataControl', Web.IArray);
---------------
将表明下列的含义:
- 类Web.UI.Data.DataControl实现了Web.IArray接口
- 类Web.UI.Data.DataControl的实例(instance)将必然具有如下方法:get_length()和getItem()
接口描述了对象实例的行为能力,只是接口机制的一个方面。在COM框架、以及一些其它高级语言的接
口语义中,接口也表明了一种实现。这种“实现”体现在三个方面:
- 用户可以通过接口,来持有一组对象方法的引用
- 用户可以通过聚合、包含和委托等技术,来使对象包含多个接口
- 用户代码可以通过一个接口,来查询“实现接口的对象”的更多接口(以及行为能力)
例如(delphi source):
---------------
type
IMyObject = interface
// [a guid for COM framework]
function run(): boolean;
end;
TMyObject = Class(TObject, IMyObject)
//...
end;
var
obj : TMyObject;
intf : IMyObject;
// ...
obj := TMyObject.Create;
if obj.GetInterface(IMyObject, intf) then
intf.run();
---------------
在这个例子中,接口IMyObject可以用于变量的类型声明。而将intf声明为IMyObject类型,也表明
intf接口指针“从对象实例中取得有效的接口实现”。GetInterface()是COM中的QueryInterface()
的另一个实现,用于从obj中取接口引用。
一旦intf变量成功的获取一个“已实现的接口(一组对象方法指针的引用)”,那么接下来就可以调
用这些接口方法了,例如执行:intf.run()。
三、Qomo中的接口之一:接口的基本语法及实现
~~~~~~~~~~~~~~~~~~
1. 接口声明
----------
Qomo中的接口声明比较简单,采用与altas(以及其它OOP框架)兼容的代码:
---------------
IMyObject = function() {
this.method = Abstract;
}
IMyObject2 = function() {
this.method = Abstract;
this.method2 = Abstract;
}
---------------
Abstract是一个在Qomo中全局提供的、类似关键字的函数。这在讲述OOP框架时已经提及过。不过它
现在被从Object.js中移到了Interface.js中。Abstract()调用的结果是触发一个异常,这也意味着
使用new()创建出的任何一个接口实例,其方法调用都将失败。
Qomo中允许使用类似继承的方式来声明接口。但Qomo不推荐接口继承,因此如果你希望接口“继承
自”其它接口,那么建议用下面的代码来声明:
---------------
IMyObjectEx = function() {
IMyObject.call(this);
this.method_ex = Abstract;
}
---------------
使用显式的IMyObject.call()的原因,是使得开发人员在阅读代码时能清晰地了解这里有一个继
承关系。——换而言之,父级接口的修改将影响到子级接口的声明。
当然,这种方式也给一些接口声明带来了方便,例如在Interface.js中声明的:
---------------
IAspectedClass = function() {
IClass.call(this);
IJoPoints.call(this);
}
---------------
最后补充一点:不要在接口声明中对构造器加入口参数。因为接口不需要、也不应当通过可变参
数来“创建”。
2. 接口注册
----------
Qomo中接口注册有两种形式:
- Interface.RegisterInterface(obj, intf1 [, intf2 ... intfn]);
- Class(<BaseClass>, <ConstructorName> [, intf1 .. intfn]);
第一种形式可以为任何的JavaScript对象,包括原生的对象、函数等注册接口。这种形式存在一
个完全等义的全局函数RegisterInterface()。——事实上这指向同一个函数——在Qomo的内部代
码中,只使用Interface.RegisterInterface()这种完整形式。
第二种形式在Object.js中实现。Interface.js中用了少量的代码来使得“类的所有实例、和子类
拥有相同的接口”。如果你使用Qomo的OOP系统,那么只需要在调用Class()进行类注册的时候加
上一个实现的接口表即可。
可以通过Aggregate()来注册“聚合的”接口,这一部分放在后面单独讲述。
作为特例:对undefined和null对象注册接口返回一个-1值。
3. 接口查询
----------
Qomo中可用下面的形式来获取对象(含Qomo和原生的JavaScript对象)的注册接口:
- Interface.QueryInterface(obj, aInterface);
与注册过程相同,接口查询也有一个完全等义的全局函数QueryInterface(),并且Qomo内部只使
用完整形式的调用。
QueryInterface()将返回一个接口的实现,也就是一个可以调用的接口(对象/指针)。例:
---------------
function MyObject() { }
TMyObject = Class(TObject, 'MyObject');
var obj = new MyObject();
var intf = QueryInterface(obj, IObject);
document.writeln(intf.hasAttribute('Name'));
// output: false
---------------
如果查询null或undefined的接口,或者传入了无效的、未注册的接口同,QueryInterface()将
抛出异常。如果返回undefined,则表明不包含该接口。——此时在用户代码中,该接口引用应
该不被继续处理,而不是盲处理。
QueryInterface()能够查询通过Aggregate()来注册“聚合的”接口。
4. 其它
----------
接口系统提供一个函数来检测一个“声明”是否是接口。如果要在QueryInterface()之前检测一
个接口是否被注册过,可以使用如下函数(返回true/false):
- Interface.IsInterface(intf);
如果一段声明代码没有(隐式地)被注册到Qomo系统中,那么使用该声明来QueryInterface()会导致
一个异常。——将“接口声明”注册到Qomo的行为隐式地包含在RegisterInterface()和Class()等
调用的过程中。
如果你真的需要在一个接口被实现之前就注册它,那么你可以将它注册给IInterface。尽管这看起
来不怎么合理,但在Qomo中,这是唯一能被理解的方式:
---------------
IMyCustomIntf = function() { ... };
Interface.RegisterInterface(IInterface, IMyCustomIntf);
---------------
在Qomo内核中是具有“将声明直接注册(封装)成接口”的能力的,这就是Interface.js中实现过的
warpInterface()函数。但它不提供给用户代码使用。系统中通过这种方法注册的接口包括:
- IInterface
- IJoPoint
- IMuEvent
- IJoPoints
如果需要,你可以将自己需要(预先)注册的接口加入到Interface.js的代码中。当然,这并不是一
种推荐的形式。
Qomo提供一些对接口的演示和示例。在Qomo代码包中:
/Framework/DOCUMENTs/T_InterfaceQuery.html : 演示接口机制的基本原理
/Framework/DOCUMENTs/T_InterfaceAggregate.html : 演示脱离Qomo使用接口、聚合的基本使用
/Framework/DOCUMENTs/BaseObjectDemo4.html : Qomo中的接口系统的使用示例和基本特性
四、Qomo中的接口之二:(内部)聚合接口的语法
~~~~~~~~~~~~~~~~~~
在COM中,分离接口的方法包括“聚合”和“包含”;在Delphi中,在编译器级别提出了“委托”来
实现接口分离的技术。三者之间只是实现技术和时间上的差异,其效果是基本一致的。
基本上来说,接口隔离原则(Interface Segregation Principle)基于“使用多个专门的接口比使用
单一的总接口总要好”。因此,这事实上是使一个对象同时具有多个接口的技术。在Qomo中,下面两
种语法都可以实现这个目标:
- Interface.RegisterInterface(obj, intf1 [, intf2 ... intfn]);
- Class(<BaseClass>, <ConstructorName> [, intf1 .. intfn]);
然而,这两种接口注册的方式要求对象/类显示的具有接口指定的方法。也就是说:如果对象obj注册
了接口intf,则 intf.XXXXX 方法将直接指向obj.XXXXX方法。
然而,我们举个例子来说: TMyObject作为类,总是有对象构造过程的,这表明TMyObject应该具备这
样接口:
---------------
IObjectConstructor = function() {
this.OnNewInstance = Abstract;
}
---------------
但我们也知道,我们不能把一个OnNewInstnace事件放在TMyObject类上,或者放在obj实例上。——因
为它并不是实例的使用者所关注的。而且,更重要的是,“对象构造过程”在Qomo中被隐含在Class()
的实现代码内部,我们需要一种机制将它的这个能力public出来,这样才能做到“接口是实现”。
在Delphi中,接口的声明方法可以“在对象的私有域中实现”。这时可以通过接口“在对象/单元外”
访问该方法。这体现了“接口表现对象的能力,而不关注具体实现(是私有、公有或者委托)”这一接口
的基本原则。
在Qomo中,综合上述的这些问题及其解决方案,实现了“(内部的)聚合接口”这一特殊的机制。
1. 声明聚合(在函数内部)
----------
Qomo的聚合首先是“为了表现目标的内部具备的行为能力”而提供的。而JavaScript中,提供行为能
力的,只有对象(构造器)和函数。因此,“声明聚合”的行为通常应当发生在一个函数的内部。例如:
---------------
function MyFunc() {
// Aggregate()与RegisterInterface()有相同的入口参数
var intfs = Aggregate(MyFunc, intf1 [, intf2 .. intfn]);
}
---------------
由于这会导致每次执行MyFunc都会调用Aggregate(),因此Qomo推荐如下的语法来声明一个带有聚合接口
的函数:
---------------
MyFunc = function () {
function _MyFunc() {
// (函数自身的实现代码...)
}
var intfs = Aggregate(_MyFunc, intf1 [, intf2 .. intfn]);
// (实现接口 ...)
return _MyFunc;
}();
---------------
由于这样做仅是为了避免重复地调用Aggregate(),因此事实上用户也可以这样来声明(尽管同样麻烦):
---------------
function MyFunc () {
if (a_aggregated_tag == false) {
var intfs = Aggregate(_MyFunc, intf1 [, intf2 .. intfn]);
// (实现接口 ...)
a_aggregated_tag = true;
}
// (函数自身的实现代码...)
}
---------------
2. 实现接口
----------
Aggregate()只是建立对象与接口之间的关系并返回一个“Interfaces对象”,这个接口集合中所有的
接口都是未被实现过的。这仍然需要由用户代码来完成,例如:
---------------
IMyFunc = function() {
this.getLength = Abstract;
}
MyFunc = function () {
var arr = new Array(5);
function _MyFunc() {
// (函数自身的实现代码...)
}
// 0). 声明聚合
var intfs = Aggregate(_MyFunc, IMyFunc);
// 1). 取指定接口的一个引用
var intf = intfs.GetInterface(IMyFunc);
// 2). 实现接口方法
intf.getLength = function() {
return arr.length;
}
return _MyFunc;
}();
---------------
上面的代码中,“Interfaces对象”intfs的GetInterface()方法用于得到一个接口的引用,而接下来的
代码则描述该接口如何被实现。——在这里,可以使用“聚合”、“包含”或者“委托”这些技术之一。
(目前,)Qomo没有直接提供这三种技术的实现方案,只是“老老实实地”逐一实现了该接口的各个方法。
3. 使用接口
----------
前面已经提到过,Aggregate()内部聚合的接口的使用,与普通注册的接口是一样的。如上例:
---------------
var intf = Interface.QueryInterface(MyFunc, IMyFunc);
document.writeln('the length is: ', intf.getLength());
---------------
五、Qomo中的接口之三:接口的实现
~~~~~~~~~~~~~~~~~~
接口的使用示例请参见/Framework/DOCUMENTs/BaseObjectDemo4.html。内部聚合技术的使用示例可
参见/Framework/DOCUMENTs/T_InterfaceAggregate.html。下面讲述Qomo中实现这些技术的一些细节。
1. 基本接口功能的实现
----------
Qomo中的接口采用这样的结构来实现Interface关键字及相关的语法:
---------------
Interface = function() {
//...
function _Interface(obj) { /* Intf1 .. Intfn */
//...
}
_Interface.QueryInterface = function(obj, intf) { ... }
_Interface.RegisterInterface =_Interface;
_Interface.IsInterface = isInterface;
return _Interface;
}();
---------------
其中_Interface()用于实现RegisterInterface()与Interface(),它们是等义的。_Interface()首先
调用warpInterface()将除第一个参数之外的其它参数转变成接口,这其实是在该接口声明(函数)上附
加一个'_INTFHANDLE_'属性。——这是目前为止Qomo中唯一尝试改变对象对外的属性的地方。
由于接口本身只是“声明”而不能直接引用,因此加上该属性并不会导致什么麻烦。在warpInterface()
对该属性做了检测,以避免用户通过修改这个值来伪造、套取接口。——尽管这对用户来说也没有什么
意义。^.^
warpInterface()是真正实现RegisterInterface()的关键代码。_Interface()中的另一部分代码用于处
理与"聚合接口(Aggregate)"相关的一些逻辑。——这在后面进一步叙述。
接口中另一个重要的函数是_Interface.QueryInterface。它的实现相对要复杂些,因为它要处理:
- 对象的接口可以在类注册中通过Class()注册到指定的类,而不必为每一个对象实例注册;
- Qomo对象或者其它JavaScript原生对象都可以具有自己的接口注册;
- 函数可以注册内部的聚合接口。
QueryInterface()必要有能力从所有这些可能注册过接口的“对象”中查询到接口。而且还要返回该
接口的一个“有效的、能调用接口方法的”实现。
“实现接口”的基本原理可以参见/Framework/DOCUMENTs/T_InterfaceQuery.html。其基本步骤是:
- 创建指定接口的实例:intf = new IYour_Intf();
- 为接口的每一个“虚方法”重新封装一个调用方法,该方法将实现该接口的对象的同名方法;
- 特殊处理:任何对象都可以有IInterface接口,将持有QueryInterface()的一个引用。
2. 聚合接口功能的实现
----------
在对_Interface()的实现过程中特殊处理了Aggregate():
---------------
// register aggregated interfacds. is special!
if ((typeof obj == 'function') || (obj instanceof Function)) {
if (_Interface.caller===Aggregate && this!==Aggregate) {
// ...
}
}
---------------
也就是说如果:
- "obj"是一个函数对象,且
- 通过调用Aggregate()函数来注册它的接口,且
- this对象不是Aggregate()自身
则_Interface()将尝试把目标对象"obj"与一个"Interfaces"对象(this)分别添加到两个数组:
---------------
$Aggrs.push(obj); // 拥有接口的对象(总是一个函数)
$Aggri.push(this); // 所拥有的接口集合对象
---------------
而在Aggregate()工具函数中,将两次调用Interface.RegisterInterface(),也就是前面提到的
_Interface()函数。这两次调用的作用并不一样:
第一次采用标准格式调用RegisterInterface(),这时传入的this将是_Aggregate()函数的引用:
---------------
Interface.RegisterInterface.apply(_Aggregate, arguments);
---------------
代码中,由于Aggregate()与RegisterInterface()的入口参数声明是一样的,因此apply()的第
二个参数直接使用了arguments。这样就首先将接口注册到了指定的对象上。
第二次采用特殊格式调用RegisterInterface(),这时传入的this将是“接口集合”Interfaces
的一个实例,而唯一的一个入口参数foo,将指向被注册接口的函数对象引用:
---------------
return Interface.RegisterInterface.call(new Interfaces(arguments), foo);
---------------
这次调用,RegisterInterface()将返回传入的this,也就是“接口集合”对象。这样做只是为了
省掉在_Aggregate()中一个临时变量。
上面的代码流程表明,一个“(内部的)聚合接口”至少具有拥有如下信息:
- 一个Interfaces对象。用于存放所有实现过的接口列表;
- 在_Interface()中的Interfaces与foo的对照表“$Aggrs与$Aggri”中存放一对信息。用于实
现getAggrInterfaces()函数,并用于支持QueryInterface()对该函数foo的查询;
- 在实现接口的函数内部,持有Interfaces对象的一个引用。它用于各接口的实现。
五、其它
~~~~~~~~~~~~~~~~~~
1. 缺省注册的接口
----------
Qomo为的一些全局对象、函数和基类注册了缺省的接口。这些接口定义在Interface.js中。缺
省注册的对象包括:
注册到 接口 说明
--------------------------------------------------------------------
* IInterface 包括undefined、null在内的所有JS原生对象和Qomo对象等
$import() IImport (beta版暂未实现)
MuEvent() IMuEvent 所有多投事件句柄: 由MuEvent()构造的实例(beta版暂未实现)
JoPoint() IJoPoint 所有的联接点: 由JoPoint()构造的实例
TObject() IObject Qomo OOP系统的基类
TClass IClass 所有通过Class()注册的类
Class() IClassRegister 类注册函数
Class()等 IJoPoints Qomo内部为一些能提供AOP能力的函数注册了IJoPoints接口
Aspect() IAspect Qomo AOP系统的切面基类
2. 如何为特定的对象(如JavaScript原生对象)注册接口
----------
在Qomo的(目前的)发布版本中,对一些JavaScript原生对象未提供接口支持。例如对Array()对象。
这只是因为Qomo没有觉察到这样做的必要,因而没有加入相关的代码。这在技术实现上其实非常简
单。下面参考对MuEvent()的实现,介绍一下Array()的接口实现方法:
---------------
// 以下代码填写在interface.js中
// 1. 接口声明
IArray = function() {
this.push = Abstract;
this.pop = Abstract;
// ...
}
// 2. Interface()的实现代码中,warpInterface()函数声明之后加入代码
warpInterface(IArray);
// 3. 在isImplemented()的实现代码中,修改(添加)如下代码
switch (intf) {
// ...
case IArray: return this instanceof Array;
}
---------------
这样就完成了。
3. 将来的版本
----------
可能在更晚些的版本中,Qomo会提供“聚合”、“包含”和“委托”这些技术的完整、具体的实
现(例如提供工具函数)。但目前的版本中,只提供了对“(内部的)聚合接口”这种需求的解决方法。
在Qomo V1正式发布的版本中,将包含对接口中的get/setter的实现。目前用户仍然只能使用类似于
intf.getLength()这样的方法去访问特性(attribute)。除此之外,接口中的“索引指示器”仍在考
虑之中。
与C#等高级语言一样,Qomo的接口对“方法、属性、索引指示器和事件”做出了思考或者实现,但
不支持对“常量、域、操作符、构造函数或析构函数”这些进行接口声明。这是“接口”本身的语
义所决定的。