面向对象的LotusScript(六)之为自定义对象模拟事件和面向对象设计与事件两篇文章都提到事件是编程时对很多场景的抽象和解决模式,核心就是在两个对象之间建立一种“提醒”机制,当某件事发生时,作为发布者的对象提醒称为收听者或预订者的对象,后者执行特定的操作。在汽车火车上,一名乘客甲请讬乘务员或者另一乘客乙到某站丙时提醒自己下车,就可以作为以上事件概念在生活中很好的原型。这里汽车到达丙站就是事件,乘客甲是收听者,乙是发布者,甲的请讬是预订或登记事件处理程序;到达丙站,事件触发;乙提醒甲,甲执行处理程序——下车(唯一的细微差别是这里的事件发布者乙并没有导致事件发生即到达丙站)。
很多编程语言都预定义了大量的事件和发布者,例如图形界面上的各种控件及其事件。在运用这些事件以外,开发中有些场合为建模的对象自定义事件有助于设计出角色清晰的对象和它们之间有效的沟通。事件的机制既已清楚,自定义事件只需对象和函数这样基本的元件。有些语言显示地提供了自定义事件的语法,其他很多则需程序员根据事件的机制设计或者模拟。本文列举了Java、JavaScript和LotusScript三种语言中自定义事件的代码以说明。
参照java.util.EventObject、javax.swing.event.EventListenerList的JavaDoc,不难编写出Java中的自定义事件示例。
package starrow.demo; import java.util.EventObject; //Declare the event object passed as argument to the listener's handler method. public class MyEvent extends EventObject { public MyEvent(Object source) { super(source); } } package starrow.demo; import java.util.EventListener; //Declare the custom listener interface. public interface MyEventListener extends EventListener { public void myEventOccurred(MyEvent evt); } package starrow.demo; //This class publishes the custom event MyEvent. public class MyEventPublisher { protected javax.swing.event.EventListenerList listenerList = new javax.swing.event.EventListenerList(); public void addMyEventListener(MyEventListener listener) { listenerList.add(MyEventListener.class, listener); } public void removeMyEventListener(MyEventListener listener) { listenerList.remove(MyEventListener.class, listener); } private void fireMyEvent(MyEvent evt) { MyEventListener[] listeners=listenerList.getListeners(MyEventListener.class); for (MyEventListener listener : listeners){ try{ listener.myEventOccurred(evt); }catch (Exception e){ e.printStackTrace(); } } } public void run(){ //Fires MyEvent fireMyEvent(new MyEvent(this)); } } package starrow.demo; public class MyEventTester { public static void main(String[] args){ MyEventPublisher publisher = new MyEventPublisher(); // Register for MyEvent from publisher publisher.addMyEventListener(new MyEventListener() { public void myEventOccurred(MyEvent evt) { System.out.println("MyEvent was fired."); } }); publisher.run(); } }
运行结果就是打印出MyEventwas fired.上面的代码是针对一个特定的事件MyEvent,如果发布者要声明另一个事件MyEvent2,就须重复写一套类似的代码。我们可以把事件的名称作为参数传递,写出更一般化的自定义事件代码。
package starrow.util; import java.util.HashMap; /** * This class contains information relevant to the event, * and is passed to the handler of the listener. * */ public class EventArg { //The event publisher. Object source; //A map used to hold event relevent information. HashMap<String, Object> info; public EventArg(Object source, HashMap<String, Object> info){ this.source=source; this.info=info; } public EventArg(Object source){ this.source=source; this.info=new HashMap<String, Object>(); } public Object getSource() { return source; } public HashMap<String, Object> getInfo() { return info; } //Export the get method of the inner map. public Object get(String key){ return info.get(key); } //Export the put method of the inner map. public EventArg put(String key, Object value){ info.put(key, value); return this; } } package starrow.util; /** * Any custom listener must implements this interface. */ public interface IEventListener { void handleEvent(EventArg ea); } package starrow.util; import java.util.ArrayList; import java.util.Collection; import java.util.Hashtable; /** * This class contains the methods needed for adding, removing event listeners, * and firing a custom event. */ public class EventPublisher { //Use a hashtable to hold the collections of event listeners, which does not permit null values. Hashtable<String, Collection<IEventListener>> eventMap =new Hashtable<String, Collection<IEventListener>>(); public void addEventListener(String eventName, IEventListener listener){ // Collection<EventListener> eventCollection; // if (eventMap.containsKey(eventName)){ // eventCollection=eventMap.get(eventName); // }else{ // eventCollection=new ArrayList<EventListener>(); // eventMap.put(eventName, eventCollection); // } Collection<IEventListener> listeners=eventMap.get(eventName); if (listeners==null){ listeners=new ArrayList<IEventListener>(); eventMap.put(eventName, listeners); } listeners.add(listener); } public void removeEventListener(String eventName, IEventListener listener){ Collection<IEventListener> listeners=eventMap.get(eventName); if (listeners!=null){ listeners.remove(listener); } } //Fires an event with the provided name and EventArg object. protected void fireEvent(String eventName, EventArg ea){ Collection<IEventListener> listeners=eventMap.get(eventName); if (listeners!=null){ for (IEventListener listener : listeners){ try{ listener.handleEvent(ea); }catch (Exception e){ e.printStackTrace(); } } } } //Fires an event with the provided name and a default EventArg object. protected void fireEvent(String eventName){ this.fireEvent(eventName, new EventArg(this)); } }
我们来看看上述自定义事件框架的一个测试。给一个发布者定义两个事件MyEvent1和MyEvent2,并分别给它们登记一个和两个收听者。
package starrow.test; import starrow.util.EventArg; import starrow.util.EventPublisher; public class MyEventPublisher extends EventPublisher{ public void run(){ System.out.println("A MyEventPublisher publisher1 is running."); System.out.println("Publisher1 fires MyEvent1."); this.fireEvent("MyEvent1"); System.out.println("Publisher1 fires MyEvent2."); EventArg ea=new EventArg(this); ea.put("name", "MyEvent2").put("x", 10).put("y", 30); this.fireEvent("MyEvent2",ea); } } package starrow.test; import starrow.util.*; public class MyEvent1Listener1 implements IEventListener{ @Override public void handleEvent(EventArg ea) { System.out.println("MyEvent1 handled in MyEvent1Listener1."); } } package starrow.test; import starrow.util.*; public class MyEvent2Listener1 implements IEventListener{ @Override public void handleEvent(EventArg ea) { System.out.println("MyEvent2 handled in MyEvent2Listener1."); } } package starrow.test; import starrow.util.*; import starrow.util.IEventListener; import java.util.Map.Entry; public class MyEvent2Listener2 implements IEventListener{ @Override public void handleEvent(EventArg ea) { System.out.println("MyEvent2 handled in MyEvent2Listener2. Event information: "); for (Entry<String, Object> entry : ea.getInfo().entrySet()){ System.out.println(entry.getKey() + ": " + entry.getValue()); } } } package starrow.test; import starrow.util.*; public class MyEventTester { public static void main(String[] args){ MyEventPublisher publisher=new MyEventPublisher(); IEventListener listener1=new MyEvent1Listener1(); publisher.addEventListener("MyEvent1", listener1); publisher.addEventListener("MyEvent2", new MyEvent2Listener1()); publisher.addEventListener("MyEvent2", new MyEvent2Listener2()); publisher.run(); // A MyEventPublisher publisher1 is running. // Publisher1 fires MyEvent1. // MyEvent1 handled in MyEvent1Listener1. // Publisher1 fires MyEvent2. // MyEvent2 handled in MyEvent2Listener1. // MyEvent2 handled in MyEvent2Listener2. Event information: // name: MyEvent2 // y: 30 // x: 10 } }
程序运行的结果附在MyEventTester的最后。依据面向对象设计与事件里对Java和C#事件编程的比较,同样可以C#写出类似的自定义事件框架,因为事件处理程序能以方法为单位添加和删除,并且多个处理程序能包容于一个收听者,所以代码会简短一些。
面向对象的LotusScript(六)之为自定义对象模拟事件一文给出了LotusScript中编写自定义事件的代码,在次重复一次:
Public EventResult As Boolean Public Class EventPublisher '定义通用的事件列表 Private EventList List As Variant '添加事件处理程序 Public Function AddEventHandler(eventName As String, handler As String) Dim handlerList List As String Dim v As Variant If Iselement(EventList(eventName)) Then v=EventList(eventName) v(handler)=handler EventList(eventName)=v Else handlerList(handler)=handler EventList(eventName)=handlerList End If End Function '去除事件处理程序 Public Function RemoveEventHandler(eventName As String, handler As String) '需要在(Options)中添加%INCLUDE "lserr.lss" On Error ErrListItemDoesNotExist Goto ExitFunction Dim handlerList As Variant handlerList=EventList(eventName) Erase handlerList(handler) EventList(eventName)=handlerList ExitFunction: Exit Function End Function '运行EventList中某事件的所有处理程序 Private Sub OnEvent(eventName As String) If Iselement(EventList(eventName)) Then Dim v As Variant v=EventList(eventName) Forall handler In v Execute handler End Forall End If End Sub End Class
同时该文也说明了这个框架的局限性:事件处理程序仅仅通过一个字符串来传递,无法检查类型和签名,缺乏安全性。只有通过公共变量才能在事件源和消费者之间传递事件的相关信息。事件处理程序必须定义在一个Script Library中,事件源才能通过Use语句引用并访问。这些限制的根源就是LotusScript里没有原生的传递函数的机制,这一点和Java相似,所以不得已利用了即时执行程序的Execute语句。其实我们也可以参照Java的事件编程所用的方法,把处理程序包含在收听者以内,将其以对象而非函数的形式登记到事件的发布者。如此,原来的局限性都迎刃而解,有兴趣的朋友可以试试。
本文给出的Java的自定义事件框架虽然允许发布者灵活声明事件,但它的可重用性却还是受到语言本身的限制。任何想利用此框架发布事件的类都须继承starrow.util.EventPublisher类,但Java只允许单重继承,也就是很多已经在现有的类层次中有父类的类型无法利用EventPublisher的代码。C#的情况也一样。这其实是此类单重继承的面向对象语言的共有问题——代码重用的可能性受到制约。解决的选项有几个,在静态类型语言里或者采用C++这样更复杂、有争议的多重继承机制,或者引入Mixin(http://en.wikipedia.org/wiki/Mixin)之类的新途径;如果语言本身是动态类型的,比如JavaScript这样的原型风格的面向对象机制,答案就很简单。下面的代码展示了一种JavaScript中的自定义事件框架,它具有和Java版本同级别的灵活性,又方便任何自定义对象“安装”以获得自定义事件的能力。
var eventify = function(obj){ var _listeners = {}; obj.addListener = function(type, listener){ if (!_listeners.hasOwnProperty(type)) { _listeners[type] = []; } _listeners[type].push(listener); return this; } obj.fire = function(event){ if (typeof event === "string") { event = { type: event }; } if (!event.type) { throw new Error("Event object missing 'type' property."); } event.target = this; if (_listeners.hasOwnProperty(event.type)) { var listeners = _listeners[event.type]; for (var i = 0, len = listeners.length; i < len; i++) { listeners[i].call(this, event); } } return this; } obj.removeListener = function(type, listener){ if (_listeners.hasOwnProperty(type)) { var listeners = _listeners[type]; for (var i = 0, len = listeners.length; i < len; i++) { if (listeners[i] === listener) { listeners.splice(i, 1); break; } } } return this; } return obj; };
将任何对象传递给eventify函数,返回的对象就具备了发布事件的能力。添加和删除处理程序的方法分别为addListener和removeListener,触发事件的方法为fire。从发布者传递给收听者的事件参数对象event有两个属性type和target,分别为事件的名称和发布者。下面就是应用上述事件框架的一个简单例子。person自定义对象从eventify方法返回后声明了一个hunger事件,处理程序在Firefox浏览器的控制台打印出I'm hungery.
var person={ name:"Jack", age:27 } eventify(person); person.addListener("hunger", function(){console.log("I'm hungery.");}); person.fire("hunger");