在上回的blog中,我抱怨过用Java用内部类来实现事件回调的机制是多么难看和麻烦。在这段时间里,我一直在考虑是否有什么方法可以不用内部类而实现同样的效果。因为Java语言本身的限制,所以常规方法是行不通的。有人建议用反射——的确通过反射可以调用任意的方法,但是反射效率不佳,对频繁发生的事件或许不太合适。动态代理也不能解决方法映射的难题。我似乎走进了死胡同。
既然此路不通,那么C#是如何实现delegate的呢?过去也曾听闻过一些内幕,不过这次被逼才真的下决心认真去看这方面的东西。原来M$使用的是代码生成的技术:对于每个delegate,C#都会为它生成一个派生于MulticaseDelegate的对象,其中实现了一个和delegate签名相同的方法。同时,对delegate的操作符+=和-=也会被编译器处理成对MulticaseDelegate方法的调用。
知道了这一点,接下来就需要看看Java中有没有类似的代码生成技术了。有意思的是,查找的时候发现有消息说,Java 6.0(Mutang)中将会提供动态代码生成的功能。这的确很吸引人,不过Java6还在Beta阶段,眼下还指望不上。其他比较出名的方法就是Apache becl和Objectweb ASM了。这两个库都比较底层,不过还有一个开源的项目——cglib——它在内部使用了asm,不过提供了较多的实用功能。据说Hibernate和Spring都用到了这个东西。研究这个库的时候,我一眼看到了MethodDelegate类——很明显这就是我要找的东西了。
MethodDelegate的设计思想很类似于C#的delegate——将接口调用转发给类的一个成员函数。不过阅读文档的时候我发现一个问题。MethodDelegate要求其所实现的接口必须只有一个公共方法,但是SWT中的许多事件接口都有不止一个方法;比如,SelectionListener就有widgetSelected和idgetDefaultSelected两个方法。因此要在SWT中使用MethodDelegate,还
必须再多实现另外一层转发。
了解手段,接下来的事情就不难了。总结起来,需要的步骤大致如下:
1、为每种需要实现的事件声明一个接口。这是MethodDelegate的要求。
2、用一个类实现SWT的事件接口,并将特定的接口调用转发到第一步所实现
的接口。
3、用MethodDelegate提供的方法,声明事件处理对象(Event Handler
Target,通常为主窗体或主部件)要实现上述的事件接口。下面就来实现一下。为了简单起见,将需要实现的接口声明为事件转发类的内部接口,以避免维护太多接口文件(因为该接口只需要声明一个方法,所以不会把外部类搞得太过复杂。)例如,处理部件选择事件(widgetSelected)的类可以如下实现:
package org.yuhao.swt.events;
import net.sf.cglib.reflect.MethodDelegate;
import org.eclipse.swt.events.*;
public class WidgetSelectedHandler implements SelectionListener
{
public WidgetSelectedHandler( Object target, String
methodName )
{
delegate = (IWidgetSelectedDelegate)
MethodDelegate.create( target,
methodName,
IWidgetSelectedDelegate.class );
}
public void widgetDefaultSelected( SelectionEvent e )
{
}
public void widgetSelected( SelectionEvent e )
{
delegate.invoke( e );
}
public void invoke( SelectionEvent e )
{
}
public interface IWidgetSelectedDelegate
{
void invoke( SelectionEvent e );
}
private IWidgetSelectedDelegate delegate;
}
这个接口虽然只有外部类用到,但是必须声明为public的,否则运行会出错(我想大概是因为代码生成以后还是外部类,需要公开访问权限)。为了简化调用,再声明一个处理事件的辅助类EventHandler,专门管理将各种事件转发到相应的Handler的工作:
public class EventHandler
{
public EventHandler( Object target )
{
this.target = target;
}
public void handleSelected( Button btn, String methodName )
{
btn.addSelectionListener( new
WidgetSelectedHandler( target, methodName ) );
}
private Object target;
}
这样,在窗口中就可以简单的如下处理事件:
public MainShell extends Shell()
{
public MainShell( Display display )
{
......
handler = new EventHandler( this );
handler.handleSelected( btn, "btn_clicked" );
}
public void btn_clicked( SelectionEvent e )
{
...
}
}
这里还需要注意:1、任何事件处理方法必须声明为public的。这样似乎有违面向对象的封装原则,不过实际上并不会造成什么大问题。2、事件方法的签名必须和对应的事件方法相同。例如,widgetSelected方法有一个SelectionEvent参数,那么处理该事件的btn_clicked方法也必须有且只有这一个参数。如果写错了,那么运行的时候会抛出异常,说找不到指定的方法。这还是需要程序员的细心来保证。还算幸运的是出错的提示非常明显,不必担心使用了过度复杂的技术而找不到真正的出错点。