享受代码,享受人生

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 - 213, comments - 2315, trackbacks - 162, articles - 45
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Observer Pattern in AOP

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

源代码下载
        
       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有经验的朋友能够讨论一二.