Qomolangma实现篇(七):Qomo的接口机制

一、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的接口对“方法、属性、索引指示器和事件”做出了思考或者实现,但不支持对“常量、域、操作符、构造函数或析构函数”这些进行接口声明。这是“接口”本身的语义所决定的。