Qomolangma實現篇(八):Qomo中的AOP框架 -开发者知识库

Qomolangma實現篇(八):Qomo中的AOP框架 -开发者知识库,第1张

================================================================================
Qomolangma OpenProject v1.0


類別    :Rich Web Client
關鍵詞  :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,
          DOM,DTHML,CSS,JavaScript,JScript

項目發起:aimingoo (aim@263.net)
項目團隊:../../Qomo_team.txt
有貢獻者:JingYu(zjy@cnpack.org)
================================================================================


一、Qomolangma中的AOP
~~~~~~~~~~~~~~~~~~

AOP(面向切面編程)有沒有必要在JavaScript中實現,一直以來是個問題。濫用AOP的特性,將導致系統
效率下降、性能不穩定等后果。因此在展開下面的討論之前,我需要先提醒Qomoer:盡管我們擁有了強
大的AOP框架,但如果你不足夠了解它,那么還是慎用之。

前面在講述Interface的時候提到,Qomo是鑒於AOP的需要,而為之提供了強大的Interface機制。但這並
不是說用戶需要定義很多接口,才能使用AOP。——Interface是在Qomo實現AOP中的“定制切面”時使用
到的關鍵技術,而不是用戶使用AOP時所必須的技術。

Qomo的AOP框架依賴於Qomo中提供的如下特性:
  - 接口機制:Interface.js
  - JSEnhance中的事件多投:MuEvent()
  - Qomo的OOP框架:Object.js

TODO: beta1 中,Qomo並未完成實現在Qomo框架內部的各個IJoPoints。但這完全不影響用戶使用AOP機
      制本身。因為AOP機制在beta1中已經是完整的了。


二、AOP基礎
~~~~~~~~~~~~~~~~~~

如果你需要一本專業的書籍來指導你學習AOP,那么我比較推薦《面向方面軟件開發(AOSD)》這本書。
Aspect被譯作“方面”、“切面”和“剖面”都是有的,請不要追究這個用詞。

AOSD中介紹到AOP中的幾個關鍵術語:
  - 聯接點(join point):程序的結構或者執行流中定義好的“位置”。Qomo中簡寫為JoPoint。
  - 通知(advice):在聯接點上會發生的一種行為,這種行為能力是AOP框架來提供的。
  - 編織(weaving): 將核心功能與方面組合在一起,以“產生一個(基於AOP的)工作系統的過程”。
  - 周圍、之前與之后(around, before and after):聯接點上(常見的)三種"通知(advice)"能力。

Qomo中用到的幾個名詞/術語:
  - 觀察者與被觀察者(observer/observable):一個切面中,觀察者是切面(aspect),被觀察者是
    切面(當前)攔截到的對象。
  - 切點(pointcut):與“聯接點(join point)”對應,切點是對這個“聯接點位置”的一個描述。
    AspectJ中使用“切點原語(一種表達式)”來描述pointcut,而Qomo使用一個表示名字的字符串。
  - 元數據(metadata):在處理切面或執行切面代碼時所需要的一些數據。這可以是用戶在建立切面
    時初始的任何數據,甚至是用於獲取數據的函數回調。
  - 引導(Introduction):Qomo中的一種切面事件,發生在before通知之前,可以決定切面的行為是
    否需要發生——是否需要攔截並發出通知。

另一個關鍵的名詞是"切面(Ascpect)",它首先是基於OOP體系的一個概念,切面描述的是對“一組”
對象實例的共同行為能力的“一個關注”。也就是說:如果你希望了解一些對象(無論它們是否是同
一父類/基類)的一些相類同的行為,那么你可以將這些行為發生的“位置”理解成一個“切面”。
而AOP就是一套針對這個“切面”進行編程的框架。

一個經常被提到的“切面”是“(記錄一些對象行為的)日志系統”。而在Qomo中,AOP被用來作為實
現JavaScript Profiler的基礎技術。

最后一個比較學術的名詞是“不知覺性(obliviousness)”,這是AOP的特性之一。它要求加入一段
AOP的代碼對原有系統不會產生可察覺的影響。——需要強調的是:around通知可能改變原有系統的
行為,這可能使得“不知覺性”被破壞或者產生歧義。


三、一些其它JS框架中的AOP
~~~~~~~~~~~~~~~~~~

在高級語言中被經常提及的AOP系統包括AspectJ和Spring。與之相比較,目前可見的一些其它JS框
架中提供的AOP能力就非常弱了。

影響最廣的一個JS AOP實現框架(概念化的模型)是"AOP Fun with JavaScript",你可以在這里讀到
這篇文檔的全文:
   http://www.jroller.com/page/deep/20030701

此后就有更多聲稱支持AOP的JS框架出現,例如Dojo。在Dojo中實現了對函數/方法的五種通知類型:
  - before
  - before-around
  - around
  - after
  - after-around

Dojo中采用的語法是這樣的:
---------------
observable = {
  method : function () { ... }
}
aspect = {
  func : function() { ... }
}

dojo.event.connect('before',  // 通知類型
  observable, 'method',       // 被觀察者及被觀察的方法
  aspect, 'func');            // 切面
---------------

另外一篇描述AOP實現的文檔是:
  http://www.dotvoid.com/view.php?id=43

它提供Introduction事件,Before、After和Around三種通知。但這個實現方案中切面的聲明,以及
與被觀察者之間的關系都處理得較為復雜。而且,事實上它破壞了AOP系統所要求的“不知覺性”。


四、Qomo中AOP語法
~~~~~~~~~~~~~~~~~~

Qomo中,Aspect是一個標准的Qomo對象。也就是說,Qomo中存在TAspect類及其子類。這包括:
---------------
TAspect
  - TFunctionAspect
  - TClassAspect
  - TObjectAspect
  - TCustomAspect
---------------

其中TAspect是一個抽象基類,因此你不應當創建它的實例。


  1. 創建切面
  ----------
  可以用標准的Qomo OOP語法,或者標准JavaScript語法來創建切面,例如:
---------------
var a_Aspect = new ObjectAspect();
---------------

Qomo的切面對象具有如下接口
---------------
IAspect = function() {
  this.supported = Abstract;
  this.assign = Abstract;
  this.unassign = Abstract;
  this.merge = Abstract;
  this.unmerge = Abstract;
  this.combine = Abstract;
  this.uncombine = Abstract;

  this.OnIntroduction = Abstract;
  this.OnBefore = Abstract;
  this.OnAfter = Abstract;
  this.OnAround = Abstract;
}
---------------

  2. 切點
  ----------
在使用一個已經創建的切面對象之前,你應該先了解該切面能否支持(supported)某些切點
(pointcut)。Qomo對此的約定如下:
---------------
 - support pointcut:
     for TFunctionAspect : 'Function'
     for TClassAspect  : 'Method'
     for TObjectAspect : 'Method', 'Event', 'AttrGetter', 'AttrSettter'
     for TCustomAspect : <可以通過用戶代碼為被觀察者定制切點>
---------------

下面的代碼用於檢測一個切面是否能切入某種切點:
---------------
var a_Aspect = new ObjectAspect();
alert(a_Aspect.supported('AttrGetter');
alert(a_Aspect.supported('Function');
---------------

  3. 關聯(assign)被觀察對象
  ----------
  切面要被關聯到一個或一些具體的被觀察者(observable)才會有意義。這通過assign()方
法來實現:
---------------
// assign()的語法:
// function assign(host, name, pointcut) { ... }
var a_Aspect = new ObjectAspect();
a_Aspect.assign(aObject, '<method_name>', 'Method');
---------------

對於不同的被觀察對象,host、name和pointcut的含義不盡相同。詳情如下:
  -----------------------------------------------------------------------------------------------
  observable           <host>             <name>                     <pointcut>
  -----------------------------------------------------------------------------------------------
  對象                 object instance    方法/事件/特性名         'Method', 'Event', ...
  函數                 a function         函數名                     'Function'
  類                   Qomo's class       該類實例(原型)的方法名     'Method'(only)
  支持定制切面的函數   a function         用戶設定的一個任意標簽     <host>函數內實現的JoPoint
  -----------------------------------------------------------------------------------------------

切面可以在創建時即關聯到目標。例如:
---------------
// assign()的語法:
// function assign(host, name, pointcut) { ... }
var a_Aspect = new ObjectAspect(aObject, '<method_name>', 'Method');
---------------

它的參數表與assign()是一致的。


  4. 多投事件MuEvent()的“中斷投送”特性
  ----------
  在介紹AOP的進一步特性之前,先公開Qomo中多投事件的一個未公開特性,即“中斷投送”。該
特性在以前的發布代碼中已經提供,而並非為AOP單獨實現的。假定如下代碼:
---------------
var obj = new Object();
obj.OnRun = new MuEvent();
obj.run = function() { return obj.OnRun() }
obj.OnRun.add(func_01);
obj.OnRun.add(func_02);
obj.OnRun.add(func_03);
---------------

缺省行為下,obj.run()調用將導致func_01等三個函數先后被調用,這個過程不會被打斷。而且
由於run()行為需要一個返回值,因此OnRun()調用期間,三個函數中最后一個"非undefined"的
返回值將會被傳出。——例如func_02返回了'a_string',而func_03返回的是undefined,則run()
將返回'a_string'。

上述的是MuEvent()內部的缺省機制。但是,如果我們在func_02中不希望繼續投送事件,也就是
說func_03得不到執行呢?下面的代碼解釋這一點:
---------------
function func_02 {
  // do somethings..

  if ( if_you_want ) {
    return new BreakEventCast('a_string');
  }
}
---------------

也就是說,事件響應代碼只需要返回一個BreakEventCast()的實例,即可中斷MuEvent()的繼續投
送。func_02同樣也可以返回有效值,例如'a_string';或者不傳入參數,則此前的事件響應代碼
中“最后一個‘非undefined’”的值將被返回。——上例中即是func_01的返回值。


  5. 切面上的行為:通知的事件及其響應
  ----------
  創建切面的目的,是觀察“對象(或目標)”在切面上的行為。AOP中通常用“通知”機制來使得
用戶代碼可以“響應”這些行為。在Qomo中,使用多投事件(MuEvent對象)來完成這件事。這意味
着用戶可以為一個切面定制任意多個響應:
---------------
function MyObject() {
  this.run = function() { };
}
var obj = new MyObject();

// 1. 創建切面並關聯, 添加
var asp = new ObjectAspect(obj, 'run', 'Method');

// 2. 定制切面上的行為
asp.OnIntroduction.add(func_01);
asp.OnIntroduction.add(func_02);
asp.OnAfter.add(func_03);

// 3. (測試)調用對象方法
obj.run();
---------------

這個切面上OnIntroduction的事件有func_01和func_02兩個響應函數。而OnAfter事件有func_03。
切面上這樣的行為一共有四個(Introduction, Before, After, Around),通知事件分別為:
   - OnIntroduction : 導引。切面其它行為發生之前檢測行為是否需要發生;
   - OnBefore = 切面行為前。
   - OnAfter = 切面行為后。
   - OnAround = 切面行為周圍。切面關注對象(observable)在調用之前,檢測是否需要調用。

下面的形式化的邏輯代碼,用於說明這些通知之間的關系:
---------------
var intro = OnIntroduction();
if (intro) OnBefore();

var cancel = intro ? OnAround() : false;
if (!cancel) value = call_observable_method_or_more();

if (intro) OnAfter();
---------------

上面這些切面上的事件響應函數可以得到的入口參數約定是:
---------------
TOnAspectBehavior = function(observable, aspectname, pointcut, args) {};
TOnIntroduction = function(observable, aspectname, pointcut, args) {};
---------------

我通常會把這四個參數縮寫為o, n, p, a。例如:
---------------
asp.OnIntroduction.add(function(o, n, p, a) {
  alert(n);
});
---------------

而按照MuEvent()的約定,在事件響應代碼中使用的this對象,將會是切面本身,也就是這里的
asp(即Aspect)。——切面是觀察者(observer),assign到的對象是被觀察者(observable)。

舉例來說,如果我們要構造一個切面,使其:
  - 關注於類MyObject()的所有實例中value>5的對象,並
  - 使value>10的對象的方法run()不被執行

那么可以通過如下的AOP代碼來實現:
---------------
var value = 0;
function MyObject() {
  this.run = function() {
    alert(this.value)
  }

  this.Create = function() {
    this.value = value ;
  }
}
TMyObject = Class(TObject, 'MyObject');

// 切面上的行為
var asp = new ClassAspect(TMyObject, 'run', 'Method');
asp.OnIntroduction.add(function(o, n, p, a) {
  if (o.value <= 5) return false;
});
asp.OnAround.add(function(o, n, p, a) {
  if (o.value > 10) return false;
});

// 測試
for (var i=0; i<20; i ) {
  var obj = new MyObject();
  obj.run();
}
---------------


  6. 定制連結點
  ----------
  如果試圖觀察目標的內部行為,而不是外部的方法/事件,則傳統的JS AOP機制將無能為
力。——例如我們想觀察對象在“構造過程中”發生的行為,而不是對構造結束后的所產生
的實例進行觀察。

例如,如果我們有一個MyFunc()的實現:
---------------
MyFunc = function() {
  // 1. 一些MyFunc()中的邏輯代碼
  var func_01 = function() {
    alert('hi, func_01');
  }

  var func_02 = function() {
    alert('hi, func_01');
  }

  // 2. 實現MyFunc()
  function _MyFunc() {
    func_01();
    func_02();
  }

  // 3. 返回MyFunc()
  return _MyFunc;
}();
---------------
我們需要對這個例子中的func_01()和func_02()進行觀察。但很顯然,在MyFunc()的外部是
無論如何也看不到這兩個方法的。

相較於其它AOP系統,Qomo在這方面提供了更強大的特性。Qomo允許開發人員在“當前函數
中”為外部系統定制連結點。這看起來與AOP系統的“不知覺性”有些背離,但也可能是實
現這種機制的唯一方法。——除非JavaScript解釋器內部提供等同的功能,或者單獨編寫外
部的paser。

Qomo中這個“定制連結點”的機制要求“observable有能力告知外部系統自己可提供的連接
點(Join Point)”,但是當這些連接點被AOP系統接入(或切入)時,observable卻是“不知
覺”的。這種能為被Qomo分解成兩個部分:
  - Qomo提供一組工具,來使observable可以產生連結點,並在連結點上產生通知
  - observable應當將這些連結點通過一個IJoPoints接口向外拋出

因此Qomo要求MyFunc()在實現中添加一些代碼來暴露它的連結點。這用到了三種技術:
  - 連接點(Join Points):產生可供外部使用的連結點。對外部代碼它表現為pointcut。
  - 編織(weaving):使連結點與目標的內部的“位置(或位置上的方法)”發生關系。
  - 聚合(Aggregate):Qomo使用“(內部的)聚合”來暴露一個實體內部的接口。

下面的代碼演示如何在上面的MyFunc()中定制連結點:
---------------
MyFunc = function() {
  // CustomAOP_1: 創建連接點
  var _joinpoints_ = new JoPoints();
  _joinpoints_.add('step1');  // 'step1'切點(pointcut)
  _joinpoints_.add('step2');  // 'step2'切點(pointcut)

  // CustomAOP_2: 編織(或織入)
  // 1. 對MyFunc()中的邏輯代碼
  var func_01 = _joinpoints_.weaving('step1', function() {
    alert('hi, func_01');
  });

  var func_02 = _joinpoints_.weaving('step2', function() {
    alert('hi, func_02');
  });

  // 2. 實現MyFunc()
  function _MyFunc() {
    func_01();
    func_02();
  }

  // CustomAOP_3: 聚合IJoPoints接口
  var _Intfs = Aggregate(_MyFunc, IJoPoints);
  var intf = _Intfs.GetInterface(IJoPoints);
  intf.getLength = function() { return _joinpoints_.length }
  intf.items = function(i) { return _joinpoints_.items(i) }
  intf.names = function(i) { if (!isNaN(i)) return _joinpoints_[i] }

  // 3. 返回MyFunc()
  return _MyFunc;
}();
---------------

我們看到,在這個例子中,對MyFunc()的程序原有結構並沒有太大的變化。最關鍵的
地方,是_MyFunc()、func_01()和func_02()內部實現代碼並沒有變化。

接下來,我們來創建切面,並書寫有關切面上的行為的代碼。亦即是測試MyFunc():
---------------
var asp = new CustomAspect(MyFunc, 'test_aspect', 'step1');
asp.OnAfter.add(function() {
  alert('do OnAfter');
});

// 測試
MyFunc();
---------------

測試的結果,我們發現顯示如下信息:
---------------
hi, func_01
do OnAfter
hi, func_02
---------------
這表現切面asp已經成功地切入func_01,並在它執行完之后、funct_02()執行之前調
用到了asp.OnAfter();


  8. 切面的合並(merge)和聯合(combine)
  ----------
  Qomo中的切面有四種被關注者對象:類、對象、函數和定制連接點的函數。但是AOP
的本意是不區分這些被關注者的類型的。

那么,如果使得一個切面能夠處理更復雜的observable呢?Qomo提出了切面的合並和聯
合這兩個概念。

合並,是指切面A將切面B的行為加到自身,使A擁有B的關注能力。但不改變B的能力。
聯合,是指切面A和其它切面(B,C,D, ...)的行為聯合在一起,作為A~D(或者更多)共有
的關注能力。

下圖說明這兩種技術的不同:
---------------
(images/aspect_merge_combine.png)

Qomolangma實現篇(八):Qomo中的AOP框架 -开发者知识库,第2张
---------------

如果我們要記錄一批目標的執行(例如做log系統),那么下面的Aspect()代碼可能是一
個不錯的示例:
---------------
function MyObjectEx() { }
function MyObject () {
  this.getValue = function () {
    return 100;
  }
  this.run = function() {
    alert(this.get('Value'));
  }
}
TMyObject = Class(TObject, 'MyObject');

var obj = new MyObject();
var A1 = new ObjectAspect(obj, 'Value', 'AttrGetter');
var A2 = new ClassAspect(TMyObject, 'run', 'Method');
var A3 = new CustomAspect(Class, 'a_custom_aspect', 'Initializtion');
var A4 = new FunctionAspect($import, '$import', 'Function');

A1.OnBefore.add(function(o, n, p, a) {
  document.writeln('Before: ', n, '<br>');
});

A2.OnAfter.add(function(o, n, p, a) {
  document.writeln('After: ', n, '<br>');
});

// 測試
A1.combine(A2, A3, A4);
TMyObjectEx = Class(TMyObject, 'MyObjectEx');
obj.run();
$import('2.js');
---------------


五、Qomo中AOP的實現技術
~~~~~~~~~~~~~~~~~~
AOP盡管復雜、強大,但是核心技術卻非常簡單。前面講到過AOP的通知和響應邏輯:
---------------
var intro = OnIntroduction();
if (intro) OnBefore();

var cancel = intro ? OnAround() : false;
if (!cancel) value = call_observable_method_or_more();

if (intro) OnAfter();
---------------

這樣的核心邏輯被實現在JSenhancd.js的JoPoints()和Aspect.js里的$Aspect()函數
中:
---------------
  function $Aspect(pointcut, foo) {
    var _aspect = this;
    var point = pointcut;
    var name = _aspect.get('AspectName');
    var f = foo;

    // AOP的核心邏輯
    return function($A) {
      if ($A===GetHandle) return f;

      // (略)
      return _value;
    }
  }
---------------

$Aspect()中暫存了_aspect, point, name等引用,供核心邏輯部分安全地調用。另
外也暫存了foo()的引用,亦即是Aspect()對象所關注的方法。這可以用於核心邏輯
部分調用,也用於在unassign()的時候還原被關注者。

GetHandle在上面代碼中有特殊的作用,它是在Aspect()對象中聲明的局部變量,當
調用切面的unassign()方法時,事實上會調用:
---------------
instance[n](GetHandle);
---------------
這樣的代碼instance[n]即是被AOP替換的方法,這樣的調用就會回到核心邏輯,從而
執行到下面的代碼:
---------------
    // AOP的核心邏輯
    return function($A) {
      if ($A===GetHandle) return f;
      // ...
    }
---------------

這樣就返回了最初暫存的foo()的引用。由於unassign()只需要執行:
---------------
instance[n] = instance[n](GetHandle);
---------------

即可完成操作。

由於GetHandle被穩藏在Aspect()內部,因此在外部不可能通過該對象來套取任何信
息,或者試圖跳過unassign()來破壞切面的邏輯。

類似的技巧還被用於解決在“實現篇(四)”中講述過的“多投事件”的“強壯就不
快,快就不強壯”的矛盾。在beta1中采用了上述的技巧來實現search(),達到O(1)
的性能:
---------------
MuEvent = function () {
  var GetHandle = {};

  var all = {
    length : 0,
    search : function(ME) {
      var i = ME(GetHandle), me = all[i];     // 1. 取handle, 並取值
      if (me && me['event']===ME) return me;  // 2. 復核
    }
  }

  // ...
    var ME = function($E) {
      if ($E===GetHandle) return handle;
      // ...
}();
---------------

 

六、其它
~~~~~~~~~~~~~~~~~~

  1. SmartAspect
  ----------
  我曾試圖實現出一個“智能的切面”,它可以理解入口參數的host是對象、類、函數或者是用戶定
制的。但是我在后來由於無法妥善處理AttrGetter與AttrSetter,因此放棄了這一想法。這直接使得
最終確定下來的assign()入口參數有了如今的設計。

另外一個方面的原因,是因為assign()的三個入口參數(以及其后的meta data參數),都是AOP中確定
的概念。因此將它們替換或者去除掉,未見得是合理的設計。


  2. Qomo中提供的連接點
  ----------
  Qomo中內置為以下函數提供了連接點(下表可能在今后被動態維護):
  -----------------------------------------------------------------------------------------------
  函數            連接點/切點       含義                                  其它
  -----------------------------------------------------------------------------------------------
  Class()         'Initializtion'   “類初始化過程”開始
                  'Initialized'     “類初始化過程”結束
                  'RegisterToSpc'    將類注冊到活動命名空間               beta1未提供
  cls.Create()    'Initializtion'   類(cls)開始構造一個新對象實例         (同上)
                  'Initialized'     類(cls)完成構造一個新對象實例         (同上)
  obj.Create()    'Initializtion'   “對象(obj)初始化過程”開始           (同上)
                  'Initialized'     “對象(obj)初始化過程”結束           (同上)
  $import()       'Decode'          對responseBody解碼                    (同上)
                  'HttpGet'         載入獲取url上的內容並解碼             (同上)
                  'TransitionUrl'   轉換Url地址                           (同上)
  MuEvent()       'NewInstance'     創建新的多投事件對象                  (同上)
                  'Close'           關閉多投特性                          (同上)
  -----------------------------------------------------------------------------------------------


  3. 其它之其它
  ----------
  根本上來說,Aspect的基類理解兩種目標的切面行為:方法(含事件)與特性。對於Custom類型的切面,
只能是方法。

不能在切面例程中,調用受影響的被切方法/特性。 例如在一個'Name'的'attrGetter'切面中,調用
observable.get('Name')。或者在'run'的'Method'切面中,調用observable.run()。——很顯然,這
將導致一個鎖死的遞歸。

AOP系統的其它兩個示例參見:
  /Framework/DOCUMENTs/AdvObjectDemo4.html     : Qomo中AOP的基本示例
  /Framework/DOCUMENTs/AdvObjectDemo5.html     : Qomo中AOP的合並與聯合的示例

Qomo的AOP系統可用於Qomo的OOP系統之外的其它對象與函數。盡管Qomo的AOP依賴OOP和Interface,但對
第三方系統來說,仍然不難從中分離出一個非Qomo的OOP實現的繼承關系的AOP。——當然,我想要實現
CustomAspect,仍然是需要完整的Interface特性的。

最佳答案:

本文经用户投稿或网站收集转载,如有侵权请联系本站。

发表评论

0条回复