享受代码,享受人生

SOA is an integration solution. SOA is message oriented first.
The Key character of SOA is loosely coupled. SOA is enriched
by creating composite apps.
posts - 98, comments - 2396, trackbacks - 162, articles - 45
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

Observer Pattern in AOP

Posted on 2005-09-03 21:45 idior 阅读(4183) 评论(15)  编辑 收藏

源代码下载
        
       Adrian Colyer在他的一篇随笔用最简单的话阐述了AOP的思想,其中concept:implment的概念让人眼前一亮. 其中他举了一个Observer模式的例子. 这里就围绕这个例子在Aspect#中的实现谈谈AOP的应用. 

在接触AOP之后很多人会把AOP简单的看成是方法拦截. 没错在EJB, Remoting, Spring ,JBoss的使用中你会深刻的感受到这一点, 或许这也是AOP概念的来源之一. 不过今天的例子, 将不仅仅涉及方法拦截, 同时也介绍AOP中另一个强大的功能Mixin..

      (AOP的专业术语中方法拦截和Mixin分别代表了动态横切和静态横切的概念)

自从面向对象的编程出现以来,OO 语言设计中一直存在着一个困扰人的基本问题。一方面,我们在领域分析过程中常常有意使用从多个父类继承的类。那是因为实际世界中的对象不会刚好适合一个简单的单继承层次结构。现在手机有越来越多的功能。而另一方面,在编程语言中允许多重继承的结果是语义极其复杂。在语言中引入这样的复杂性往往会使发生错误的概率增加,所以Java .Net都抛弃了多重继承的概念. 而通过聚合和接口继承在一定程度上达到多重继承的效果. 然而多重继承这块蛋糕在很多情况下又是那么的诱人.

想想在Observer模式中那个讨厌的Subject, 为了拥有添加监听者,通知监听者的功能你就得继承Subject, 这样在以单继承为基础的Java, .Net的语言中就意味着你丧失了唯一一个继承的能力. 而更气人的是这个基类与核心业务无关, 仅仅是为了实现通知的功能.

       在继续阅读之前, 请先看一个Observer的例子. 推荐你阅读全文, 如果你对Observer已经十分了解的话, 请直接看文章最后的那个关于股票的例子. 本文将以此为基础.

首先看看在例子中是如何解决Subject的问题的.

       1 // "Subject"

    2 abstract class Stock

    3 {

    4     // Fields

    5     protected string symbol;

    6     protected double price;

    7     private ArrayList investors = new ArrayList();

    8 

    9     // Constructor

   10     public Stock( string symbol, double price )

   11     {

   12         this.symbol = symbol;

   13         this.price = price;

   14     }

   15 

   16     // Methods

   17     public void Attach( Investor investor )

   18     {

   19         investors.Add( investor );

   20     }

   21 

   22     public void Detach( Investor investor )

   23     {

   24         investors.Remove( investor );

   25     }

   26 

   27     public void Notify()

   28     {

   29         foreach( Investor i in investors )

   30             i.Update( this );

   31     }

   32 

   33     // Properties

   34     public double Price

   35     {

   36         get{ return price; }

   37         set

   38         {

   39             price = value;

   40             Notify();

   41         }

   42     }

   43 

   44     public string Symbol

   45     {

   46         get{ return symbol; }

   47         set{ symbol = value; }

   48     }

   49 }

   50 

   51 // "ConcreteSubject"

   52 class IBM : Stock

   53 {

   54     // Constructor

   55     public IBM( string symbol, double price )

   56         : base( symbol, price ) {}

   57 }

  

        它直接将Subject放入了业务类Stock, 显然不符合关注点分离的思想. 即便让Stock继承Subject也会有前述之不足.

       让我们看看使用Mixin怎么解决这个问题.

       首先创建一个ISubject的接口  

     6     public interface  ISubject

    7     {

    8         void Attach(IObserver observer);

    9         void Detach(IObserver observer);

   10         void Notify();

11                  }

 

然后在SubjectMixin中实现ISubject.

    8  public class SubjectMixin : IProxyAware,ISubject

    9     {

   10         private IList observers = new ArrayList();

   11         private object proxy;

   12 

   13         public SubjectMixin()

   14         {

   15 

   16         }

   17         public void Attach(IObserver observer)

   18         {

   19             observers.Add(observer);

   20         }

   21 

   22         public void Detach(IObserver observer)

   23         {

   24             observers.Remove(observer);

   25         }

   26 

   27         public void Notify()

   28         {

   29             foreach (IObserver o in observers)

   30                 o.Update(proxy as Stock);

   31         }

   32 

   33         #region IProxyAware Member

   34 

   35         public void SetProxy(object proxy)

   36         {

   37             this.proxy = proxy;

   38         }

   39 

   40         #endregion

   41     }

这里看上去和Subject的实现没什么差别,甚至还麻烦了一点. 不过请接着看现在的Stock

   3       public class Stock

    4     {

    5         // Fields

    6         protected string symbol;

    7         protected double price;

    8 

    9         // Constructor

   10 

   11         public Stock(string symbol, double price)

   12         {

   13             this.symbol = symbol;

   14             this.price = price;

   15         }

   16 

   17         // Properties

   18         public virtual double Price // here must use virtual because of  dynamic proxy

   19         {

   20             get{return price;}

   21             set{price = value;}

   22         }

   23 

   24         public string Symbol

   25         {

   26             get{return symbol;}

   27             set{symbol = value;}

   28         }

   29     }

   30 

   31     public class IBM : Stock

   32     {

   33         // Constructor

   34         public IBM() : base("IBM", 120.0)

   35         {

   36         }

   37     }

非常的干净,只关心核心业务,没有任何的污染.

但是这样又如何让Stock实现Subject的功能呢? 你需要做两个工作:

1. 编写如下的配置文件SubjectAspect.cfg

aspect SubjectAspect for Stock

        include SubjectMixin

end

2. 在主程序中象下面这样创建Stock对象.

  

 19  AspectLanguageEngineBuilder builder = new AspectLanguageEngineBuilder(File.OpenText(@"../../SubjectAspect.cfg"));

   20             AspectEngine engine = builder.Build();      

   21 

   22             IBM ibm = engine.WrapClass(typeof(IBM)) as IBM;

   23 

   24             // Create investors

   25             Investor s = new Investor("idior");

   26             Investor b = new Investor("dudu");

   27 

   28             // Create IBM stock and attach investors

   29             ISubject sub = ibm as ISubject;

   30             sub.Attach(s);

   31             sub.Attach(b);

 

很神奇是吗? ibm对象竟然具有了ISubject的功能. (本文重点不在如何实现AOP, 而是如何运用AOP的思想, 所以在此不介绍实现原理.)

通过Mixin的运用我们得到了一个更加良好的设计. 没有污染的Stock对象, 但是却具有Subject的功能.

如果你看了开头我提到的文章, 你可能开始好奇怎么没说到如何避免到处散布的Notify方法啊? 下面就讨论这个话题.

用过Observer模式的人都知道当你的属性改变后想通知到Listener,必须在改变属性后显式的调用Notify方法来通知, 这就意味着每个希望被监视的属性都要调用这么一个方法.

Notify方法在一个类中将出现N,更变态的是这样的类还有N. 这种情况在AOP的术语中叫做Crosscuttings. 出现这种情况时, 一旦需求发生变化改变起来相当麻烦. 按照Adrian Colyer的观点concept:implement=1:1 ,在这里就是希望调用Notify的代码只出现一次.

       (: concept:implement=1:1 对于一个概念来说最好只有一处实现, 不要把概念简单的理解为方法的实现, 方法的调用也是一个概念)

       如何做到这一点? 可能的想法就是当我们修改属性时能够拦截此过程,然后加入Notify方法. 这种拦截并修改原有方法的机制在AOP称为Advice. 下面我们就来实现Observer中的NotifyAdvice.

    7     public class NotifyInterceptor : IMethodInterceptor

    8     {

    9         public NotifyInterceptor()

   10         {

   11         }

   12 

   13         public object Invoke(IMethodInvocation invocation)

   14         {

   15             object returnVal = invocation.Proceed();

   16             ISubject sub= invocation.This as ISubject;

   17             sub.Notify();

   18             return returnVal;

   19         }

   20     }


    其实这里就是一个拦截器
, 通过IMethodInvocation类型的变量我们可以获得足够的信息(被拦截方法的参数,被拦截方法所属的对象) 这样完全可以满足任何的拦截要求. 这里我们通过invocation.This获得被拦截方法所属的对象, 然后调用一个那个到处散布的Notify方法就可以了.

最后就是通过配置文件使用它.

aspect SubjectAspect for Stock

        include SubjectMixin   

        pointcut propertywrite(*)

            advice(NotifyInterceptor)

        end

end

通过pointcut我们定义了哪些方法需要被拦截, 这里简单了定义了所有包含set功能的属性都会被拦截(Aspect#支持更复杂的静态定义, 不过不支持动态定义). 这样我们就消灭了在Observer模式中到处散布的Notify方法. 

让我们来总结一下AOP是如何实现关注点分离的:

1.                       通过静态横切(Mixin) 实现了功能实现的分离. 并且完全不影响原来的对象. 使得业务对象可以更加纯净.

2.                       通过动态横切(Advice) 实现了功能调用的分离. 使得功能调用只出现一次. 并且不出现在业务对象中.

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

MIXIN和多重继承的区别:
1.
模块不产生实例
2.
模块使类层次关系保持树状,而不是网状。
3. Mixin可以访问原对象

用一个图来说明: A是原对象 BMixin  CAB Mixin后的对象

A   B                                     A <---B

  \/                                          |

  C= A +B                              C=A+B

 
Aspect#的相关问题

1. 由于采用动态代理技术, 希望被拦截的方法和属性不得不声明为virtual

18         public virtual double Price // here must use virtual because of  dynamic proxy

   19         {

   20             get{return price;}

   21             set{price = value;}

   22         }

2.    pointcuts的定义功能过于简单, 尤其不能定义动态的pointcuts.

3.  使用过于繁琐. 

 19  AspectLanguageEngineBuilder builder = new AspectLanguageEngineBuilder(File.OpenText(@"../../SubjectAspect.cfg"));

   20             AspectEngine engine = builder.Build();      

   21 

   22             IBM ibm = engine.WrapClass(typeof(IBM)) as IBM;

   既然已经在配置文件中已经定义Aspect应用的类.如下所示:

   aspect SubjectAspect for Stock
 
  那么最好在创建这种类的时候直接拦截, 实现IBM ibm=new IBM(); 这样的使用效果.
  
不过Aspect#虽然没有直接实现此功能, 不过借助它的老爸CastleIoc容器, 外加一个定制的Facility倒是可以实现前述的效果.

4.  不要在Windbey beta2下使用Aspect#. 这个浪费了我很多时间, 用windbey用惯了.

----------------------------------------------------------------------------------------------
阅读本文前的推荐阅读:
Castle与Mixin
Castle实践8-AspectSharp
AspectSharp Tutorial


本文基本上是个人观点, 没有参考什么大牛的论点.所以其中必有不到之处, 所以希望对Aop有经验的朋友能够讨论一二.

Feedback

#1楼  回复 引用 查看   

2005-09-04 18:19 by Teddy's Knowledge Base      
我觉得这虽然是一种用法,但是,也是一种比较取巧的用法。

实际上,一般情况下,在只能使用单继承的OO语言中,我们都会为可能被多继承的类定义接口,比如你说到的Subject吧,比如有这么个接口ISubject,那么我就完全可以定义一个类IBM如下:

class IBM : SomeOtherBaseClass, ISubject
{
//...
}

至于ISubject的实现,则完全可以通过聚合一个Subject实例的方式来实现。

因此,一般来讲,即使没有了多继承,我也完全可以用接口+聚合的方式来重构的。

回到Mixin本身,它包括它的兄弟interception,本质上是两种weave已有类的方式,至于你用这两种方式weave后用来做什么,它其实并不直接关心!但如果你通过它来到处用作多继承,实际上就回到了允许多继承的老路,还是拐弯抹角那种,缺点也和在编译器上支持多继承类似,甚至更不雅!

#2楼[楼主]  回复 引用 查看   

2005-09-04 19:45 by idior      
本文也是尝试性质, 有关Mixin的应用究竟应该如何值得讨论. 这里,我只是给出可能的应用, 也算开拓眼界.

teddy可以看看这篇文章
http://www-128.ibm.com/developerworks/cn/java/j-diag1203/


个人认为除了以下的安全限制, 还需要有使用Mixin的指导原则, 否则极有可能被滥用.
1. Illegal overriding/hiding. Accidental (a.k.a., "incidental") overriding of a method from a parent class is allowed if the corresponding "copied" class is legal (that is, a method would not have the same arg type, but a different return type as an overridden method in the parent class, or a different throws clause, or incompatible modifiers, such as static versus instance).


2. Ambiguous overloading. Ambiguous overloading is a problem because method arguments may be of mixin type, allowing for situations in which two overloaded methods are applicable and neither is more specific. This problem is solved by disallowing overloading if the two methods have the same number and type of arguments except for some argument for which they have two different reference types.


3. Method annotation. Inherited methods are annotated with "Parent" type.


4. Class-only instantiation. Jam mixins can be instantiated only on classes; unlike components in Jiazzi, there is no notion of mixin composition (however, the Jam team would like to explore such an extension). For more on Jiazzi and component-based programming, see Resources.


5. No "this" passing. A potential show-stopper, passing "this" as an argument to a method or a constructor from inside a mixin is forbidden! This Jam idiosyncrasy is necessary to preserve the soundness of the type system. Without it, there is no way to ensure that a Jam mixin type will be valid across all possible instantiations. Nevertheless, it's a very unfortunate restriction because it limits the set of classes eligible to be turned into mixins.

#3楼  回复 引用 查看   

2005-09-06 00:11 by dudu      
文中“Observer的例子”链接有问题。

#4楼  回复 引用 查看   

2005-09-06 09:02 by 双鱼座      
哎!强烈羡慕你可以由着自己的性子选择自己感兴趣的话题来研究,目前偶还天天痛苦地被项目牵着走。真怀念2003年年底的那段日子!
关于Remoting提供的AOP我仅限于对msdn上的那篇文章的研究。不过除了植入一个Preprocess和一个Postprocess外,如何绕过原来的方法而不throw异常,一直令我困惑。感觉这条路可能会是ms将来会强化的一条路,值得花时间去研究。
Castle我也研究过,但是基于override的DynamicProxy我不能接受。按照我的经验,最想Cut的Method往往都是Final的,如果一定要基于virtual,就只能望method兴叹了。我猜这应该也是AspectJ不使用DynamicProxy的原因之一吧!在.net使用DynamicProxy还不如java,毕竟java的Method默认的是virtual而不是final。
Mixin绝对是一种超级变态的方法,在一个不支持new操作符重载的语言体系里,Mixin的意义非常让人怀疑。
我不想颠覆Castle,相反我非常喜欢Castle。不过AOP方面我还是认为应该等待G#。.net天生与Java是不同的,Java由社区推动,而.net由MS推动。我相信有一天MS感觉到Castle的压力,会毫不犹豫地推翻Castle。MS的世界总是这样的,从没有例外过。

#5楼[楼主]  回复 引用 查看   

2005-09-06 09:23 by idior      
@dudu
已修正,谢谢提醒.

@双鱼座
呵呵, 我离你的日子也不远了, 所以争着现在多学点点自己感兴趣的东西.
1. remoting的应用仅仅提供了一套方法拦截的机制, 很多aop中的核心概念并未涉及如pointcuts的定义, introduce的应用等等. 而且效率低下. 可以说它不是为了aop而诞生的, 它的根本目的是为了远程调用.
2. 基于动态代理的方法确实有很大的缺陷. 尤其是在改造原有系统的时候, 但是如果把aop看作一种编程思想在开始新项目的时候就使用, 那么这个问题的影响就小了点. 不过我也不喜欢这种限制.
3. "在一个不支持new操作符重载的语言体系里,Mixin的意义非常让人怀疑。"
这点Castle给了我们一点提示, 通过容器的作用可以很方便的获得一个被Mixin的对象.
http://wj.cnblogs.com/archive/2005/08/23/220552.html
4. G#确实值得期待. google了一下发现国内还是我最早介绍G#的. 呵呵 :)
http://www.cnblogs.com/idior/archive/2005/04/08/134257.html
我认为应用AOP可以等待,但是学习AOP不该再等待了, 为什么我们总是落后于国外呢.

#6楼  回复 引用 查看   

2005-09-06 09:26 by wayfarer      
@Teddy:
在Observer模式下,单继承仍然是最大的障碍。虽然,我们可以通过接口来取代抽象基类。但subject的Notify方法毕竟是所有subject对象通用的。改用接口的方式,就要求所有的subject都必须实现自己的subject了。此时如果该subject还需要继承其他的类,问题就很麻烦。

采用AOP的introduce,确实可以突破单继承的局限。不过,以本文的例子来看,由于引入了配置文件,因此一旦需要introduce的类增多,这对于开发工作而言,就相对更加繁琐,并不如AspectJ的方式好。另外,我不知道这样的introduce后,性能怎样?

#7楼  回复 引用 查看   

2005-09-06 09:46 by Teddy's Knowledge Base      
就像idior说的,必须加入严格的约束,并且最好至少有编译时的约束检测,否则,大量这样实现的多继承就会是灾难了。wayfarer说的配置的问题也是问题,另外,相比dinamicproxy方案,static weave来实现mixin还是更舒服一点!

#8楼  回复 引用 查看   

2005-09-06 10:40 by THIN      
一声叹息

#9楼  回复 引用   

2006-01-16 14:20 by 金萌[未注册用户]
请问我想通过AspectJ使用LoadingTime(配置AOP.xml文件)实现向一个现有的类添加方法,静态横切再AOP.xml文件中如何写?多谢

MSN:jinmeng0521@hotmail.com

#10楼[楼主]  回复 引用 查看   

2006-01-16 14:39 by idior      
◎金萌
不好意思没有用过AspectJ

#11楼  回复 引用 查看   

2011-04-23 14:31 by 风云      
谢谢分享!