• Qomolangma实现篇(八):Qomo中的AOP框架


    ================================================================================
    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)


    ---------------

    如果我们要记录一批目标的执行(例如做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特性的。

  • 相关阅读:
    电信生命周期说明
    find in linux 2 微信公众号
    GDB中应该知道的几个调试方法 2 微信公众号
    linux 下程序员专用搜索源码用来替代grep的软件ack(后来发现一个更快的: rg), 且有vim插件的 2 微信公众号
    linux下的 c 和 c++ 开发工具及linux内核开发工具 2 微信公众号
    linux下命令行发送邮件的软件:mutt 微信公众号
    腺样体肿大的综合治疗考虑 微信公众号
    打呼噜治疗方法 微信公众号
    vim 操作 2 微信公众号
    nginx之外的web 服务器caddy 微信公众号
  • 原文地址:https://www.cnblogs.com/encounter/p/2188702.html
Copyright © 2020-2023  润新知