在C#中有一个event关键字,可以很方便的定义一个事件。
event EventHandler Something;
Something += DoSomething;
通过上面的两行,在C#中可以快速的将事件Something转发到DoSomething方法中。用习惯了这种事件的方法,反而不习惯java中的事件模型了。在Windows
Phone
7正式登录大陆之前,要暂时开发Android的游戏。为了不让以后的开发过程感觉不自在,暂时定义一个java的事件模型。
需求很清楚,要模拟C#中event的使用。首先需要定义一个事件句柄(EventHandler):
packagecom.wind.util;
importjava.util.ArrayList;
public classEventHandler {
privateArrayList listeners= newArrayList();
public synchronized voidadd(EventListener listener) {
// 判断在监听器列表中是否存在当前监听器对象if(listeners.indexOf(listener) < 0) {
listeners.add(listener);
}
}
public synchronized voidremove(EventListener listener){
listeners.remove(listener);
}
public voidinvoke(Object sender) {
// 构造事件对象EventArg e = newEventArg();
invoke(sender, e);
}
public voidinvoke(Object sender, Object... args) {
// 构造事件对象EventArg e = newEventArg(args);
invoke(sender, e);
}
public voidinvoke(Object sender, EventArg e) {
synchronized(listeners) {
// 构造事件对象for(inti = 0; i < listeners.size(); i++) {
listeners.get(i).doEvent(sender, e);
}
}
}
}
这里简单的解释一下,在java中有一个Observable类,实现了观察者模式。但是在其中有很多我用不上的方法,而且添加观察者为addObserver,看起来很不爽。索性就写了上面的这个EventHandler类。使用ArrayList来存储所有的监听器对象。提供了add和remove方法来添加和删除监听器(这里本来是采用addListener和removeListener的,但是在实际使用的时候代码过长,就去掉了)。提供了3个invoke重载来完成事件的调用,这个重载组主要是针对事件参数来进行的重载。
接下来就是定义事件参数:
packagecom.wind.util;
public classEventArg {
privateObject[] args= null;
publicEventArg() {}
publicEventArg(Object... args) {
this.args= args;
}
public intgetArgLength() {
if(args== null) {
return0;
} else{
returnargs.length;
}
}
publicObject getArg(intindex) {
if(args== null) return null;
if(args.length<= index) {
return null;
} else{
returnargs[index];
}
}
public booleanhasArg() {
returnargs.length> 0;
}
@OverridepublicString toString() {
if(args== null) return"";
returnargs.toString();
}
}
其实这个设计并不是太好,我在性能、扩展性之间进行了取舍,因为我暂时涉及不到太大的项目,而且事件的调用不是很普及(相对于每秒30次的刷新而言),将事件参数封装在Object数组args中。通过Java5的动态参数特性进行了封装。对args进行了第二层封装,并不直接对外提供。采用getArgLength和getArg两个方法配合,完成参数的获取操作,避免了对args进行非法修改。因为args本质上是Object对象,所以任何对象都可以方进去,扩展性非常高。稍微麻烦点的地方是获取参数的时候,容易引发转型错误,不过借鉴了Ruby等动态语言的设计思路后,转型错误并不是那么容易出现,毕竟事件的接收双方都是我自己定义的(这里的一个取舍在于我不是做大型商业框架,不需要考虑过多的健壮性)。
packagecom.wind.util;
public interfaceEventListener {
voiddoEvent(Object sender, EventArg e);
}
监听器EventListener是一个接口,任何接受的监听器都需要实现doEvent方法,两个参数Object
sender和EventArg
e。其实这里的sender是采用的.Net中的事件命名方式,主要是习惯了,就这样沿用了(毕竟半年后估计又转回.Net了)。光是以上的三个类、接口可能还看不出来和C#中的event有什么关联,不着急,我们来看看在实际的代码中如何使用。
首先在事件源的类中,需要定义句柄,这里我按照开篇的时候C#的代码来命名:
privateEventHandler somethingHandler= newEventHandler();
publicEventHandler something() { returnsomethingHandler; }
首先定义一个private的句柄,再通过一个something方法来返回句柄对象(这种设计模式参考Ruby中的虚方法模式,采用一个方法来模拟模拟的属性或者更改属性的名称、类型等定义)。虽然比起C#的代码要稍微繁琐一点,不过我认为这点繁琐还是可以接受的。
somethingHandler.invoke(this);
somethingHandler.invoke(this, arg1, arg2);
触发事件的过程非常简单,通过EventHandler中invoke的重载链,可以很方便的添加参数。由于采用了动态参数的特性,在调用的时候可以不用去管参数的类型和个数,用逗号隔开即可。怎样,有点感觉了吗?别着急,重点还在后面。
privateEventListener somethingListener= newEventListener() {
@Overridepublic voiddoEvent(Object sender, EventArg e) {
if(e.getArgLength() >= 2) {
System.out.println("something: arg1="+ e.getArg(0) + " arg2="+ e.getArg(1));
}
}
};
在需要监听事件的方法中,通过匿名内部类的方法实现了EventListener接口。打印了arg1和arg2。
component1.something().add(somethingListener);
添加监听的方法很简洁,是不是看到了类似C#中的语法了?可惜java不支持符号重载,要不然我也写成+=和-=的形式,呵呵。
总的来说这样封装的事件模型,效率是比较高的,由于没有采用Observable和EventObject,这三段代码和java.util的耦合是很低的,只引用了一个ArrayList类。这样在移植到Android的过程当中也不会出现什么问题。
关于效率问题,我觉得不是问题,在测试当中,一千万次委托调用,在1个监听器的情况下需要800ms,2个监听器需要1200ms。效率上是得到保障了的,在我的PC上每毫秒能执行一万次委托,这个速度可以忽略了,我相信就算在Android中,执行效率也不会太差。
2010年9月10日更新:
上次已经实现了类似C#中的事件。本着精益求精的精神,对事件模型进行了重构,首先将EventArg更名为EventArgs,名称上更加贴切。
然后对EventHandler和EventListener进行了泛型化,将EventArgs取代为,在需要使用EventArgs的情况下就声明EventHandler即可,如果需要其他类型,如Date类型的参数,只需要声明为EventHandler即可,在监听时采用EventListener,自然就接受了Date类型的参数。