原文:http://today.java.net/pub/a/today/2003/10/24/swing.html?page=2
解决方案:事件驱动编程
所有前面的这些解决方案都存在一个共同的致命缺陷--企图在持续地改变线程的同时表示一个任务的功能集。但是改变线程需要异步的模型,而线程异步地处理Runnable。问题的部分原因是我们在企图在一个异步的线程模型之上实现一个同步的模型。这是所有Runnable之间的链和依赖,执行顺序和内部类scooping问题的根源。如果我们可以构建真正的异步,我们就可以解决我们的问题并极大地简化Swing线程。
在这之前,让我们先列举一下我们要解决的问题:
1. 在适当的线程中执行代码
2. 使用SwingUtilities.invokeLater()异步地执行.
异步地执行导致了下面的问题:
1. 互相耦合的组件
2. 变量传递的困难
3. 执行的顺序
让我们考虑一下像Java消息服务(JMS)这样的基于消息的系统,因为它们提供了在异步环境中功能组件之间的松散耦合。消息系统触发异步事件,正如在Enterprise Integration Patterns 中描述的。感兴趣的参与者监听该事件,并对事件做成响应--通常通过执行它们自己的一些代码。结果是一组模块化的,松散耦合的组件,组件可以添加到或者从系统中去除而不影响到其它组件。更重要的,组件之间的依赖被最小化了,而每一个组件都是良好定义的和封装的--每一个都仅对自己的工作负责。它们简单地触发消息,其它一些组件将响应这个消息,并对其它组件触发的消息进行响应。
现在,我们先忽略线程问题,将组件解耦并移植到异步环境中。在我们解决了异步问题后,我们将回过头来看看线程问题。正如我们所将要看到的,那时解决这个问题将非常容易。
让我们还拿前面引入的例子,并把它移植到基于事件的模型。首先,我们把lookup调用抽象到一个叫LookupManager的类中。这将允许我们将所有UI类中的数据库逻辑移出,并最终允许我们完全将这两者脱耦。下面是LookupManager类的代码:
class LookupManager {
private String[] lookup(String text) {
String[] results = ...
// database lookup code
return results
}
}
现在我们开始向异步模型转换。为了使这个调用异步化,我们需要抽象调用的返回。换句话,方法不能返回任何值。我们将以分辨什么相关的动作是其它类所希望知道的开始。在我们这个例子中最明显的事件是搜索结束事件。所以让我们创建一个监听器接口来响应这些事件。该接口含有单个方法lookupCompleted()。下面是接口的定义:
interface LookupListener {
public void lookupCompleted(Iterator results);
}
遵守Java的标准,我们创建另外一个称作LookupEvent的类包含结果字串数组,而不是到处直接传递字串数组。这将允许我们在不改变LookupListener接口的情况下传递其它信息。例如,我们可以在LookupEvent中同时包括查找的字串和结果。下面是LookupEvent类:
public class LookupEvent {
String searchText;
String[] results;
public LookupEvent(String searchText) {
this.searchText = searchText;
}
public LookupEvent(String searchText,
String[] results) {
this.searchText = searchText;
this.results = results;
}
public String getSearchText() {
return searchText;
}
public String[] getResults() {
return results;
}
}
注意LookupEvent类是不可变的。这是很重要的,因为我们并不知道在传递过程中谁将处理这些事件。除非我们创建事件的保护拷贝来传递给每一个监听者,我们需要把事件做成不可变的。如果不这样,一个监听者可能会无意或者恶意地修订事件对象,并破坏系统。
现在我们需要在LookupManager上调用lookupComplete()事件。我们首先要在LookupManager上添加一个LookupListener的集合:
List listeners = new ArrayList();
并提供在LookupManager上添加和去除LookupListener的方法:
public void addLookupListener(LookupListener listener){
listeners.add(listener);
}
public void removeLookupListener(LookupListener listener){
listeners.remove(listener);
}
当动作发生时,我们需要调用监听者的代码。在我们的例子中,我们将在查找返回时触发一个lookupCompleted()事件。这意味着在监听者集合上迭代,并使用一个LookupEvent事件对象调用它们的lookupCompleted()方法。
我喜欢把这些代码析取到一个独立的方法fire[event-method-name] ,其中构造一个事件对象,在监听器集合上迭代,并调用每一个监听器上的适当的方法。这有助于隔离主要逻辑代码和调用监听器的代码。下面是我们的fireLookupCompleted方法:
private void fireLookupCompleted(String searchText,
String[] results){
LookupEvent event =
new LookupEvent(searchText, results);
Iterator iter =
new ArrayList(listeners).iterator();
while (iter.hasNext()) {
LookupListener listener =
(LookupListener) iter.next();
listener.lookupCompleted(event);
}
}
第2行代码创建了一个新的集合,传入原监听器集合。这在监听器响应事件后决定在LookupManager中去除自己时将发挥作用。如果我们不是安全地拷贝集合,在一些监听器应该 被调用而没有被调用时发生令人厌烦的错误。
下面,我们将在动作完成时调用fireLookupCompleted辅助方法。这是lookup方法的返回查询结果的结束处。所以我们可以改变lookup方法使其触发一个事件而不是返回字串数组本身。下面是新的lookup方法:
public void lookup(String text) {
//mimic the server call delay...
try {
Thread.sleep(5000);
} catch (Exception e){
e.printStackTrace();
}
//imagine we got this from a server
String[] results =
new String[]{"Book one",
"Book two",
"Book three"};
fireLookupCompleted(text, results);
}
现在让我们把监听器添加到LookupManager。我们希望当查找返回时更新文本区域。以前,我们只是直接调用setText()方法。因为文本区域是和数据库调用一起都在UI中执行的。既然我们已经将查找逻辑从UI中抽象出来了,我们将把UI类作为一个到LookupManager的监听器,监听lookup事件并相应地更新自己。首先我们将在类定义中实现监听器接口:
public class FixedFrame implements LookupListener
接着我们实现接口方法:
public void lookupCompleted(final LookupEvent e) {
outputTA.setText("");
String[] results = e.getResults();
for (int i = 0; i < results.length; i++) {
String result = results[i];
outputTA.setText(outputTA.getText() +
"\n" + result);
}
}
最后,我们将它注册为LookupManager的一个监听器:
public FixedFrame() {
lookupManager = new LookupManager();
//here we register the listener
lookupManager.addListener(this);
initComponents();
layoutComponents();
}
为了简化,我在类的构造器中将它添加为监听器。这在大多数系统上都允许良好。当系统变得更加复杂时,你可能会重构、从构造器中提炼出监听器注册代码,以允许更大的灵活性和扩展性。
到现在为止,你看到了所有组件之间的连接,注意职责的分离。用户界面类负责信息的显示--并且仅负责信息的显示。另一方面,LookupManager类负责所有的lookup连接和逻辑。并且,LookupManager负责在它变化时通知监听器--而不是当变化发生时应该具体做什么。这允许你连接任意多的监听器。
为了演示如何添加新的事件,让我们回头添加一个lookup开始的事件。我们可以添加一个称作lookupStarted()的事件到LookupListener,我们将在查找开始执行前触发它。我们也创建一个fireLookupStarted()事件调用所有LookupListener的lookupStarted()。现在lookup方法如下:
public void lookup(String text) {
fireLookupStarted(text);
//mimic the server call delay...
try {
Thread.sleep(5000);
} catch (Exception e){
e.printStackTrace();
}
//imagine we got this from a server
String[] results =
new String[]{"Book one",
"Book two",
"Book three"};
fireLookupCompleted(text, results);
}
我们也添加新的触发方法fireLookupStarted()。这个方法等同于fireLookupCompleted()方法,除了我们调用监听器上的lookupStarted()方法,并且该事件也不包含结果集。下面是代码:
private void fireLookupStarted(String searchText){
LookupEvent event =
new LookupEvent(searchText);
Iterator iter =
new ArrayList(listeners).iterator();
while (iter.hasNext()) {
LookupListener listener =
(LookupListener) iter.next();
listener.lookupStarted(event);
}
}
最后,我们在UI类上实现lookupStarted()方法,设置文本区域提示当前搜索的字符串。
public void lookupStarted(final LookupEvent e) {
outputTA.setText("Searching for: " +
e.getSearchText());
}
这个例子展示了添加新的事件是多么容易。现在,让我们看看展示事件驱动脱耦的灵活性。我们将通过创建一个日志类,当一个搜索开始和结束时在命令行中输出信息来演示。我们称这个类为Logger。下面是它的代码:
public class Logger implements LookupListener {
public void lookupStarted(LookupEvent e) {
System.out.println("Lookup started: " +
e.getSearchText());
}
public void lookupCompleted(LookupEvent e) {
System.out.println("Lookup completed: " +
e.getSearchText() +
" " +
e.getResults());
}
}
现在,我们添加Logger作为在FixedFrame构造方法中的LookupManager的一个监听器。
public FixedFrame() {
lookupManager = new LookupManager();
lookupManager.addListener(this);
lookupManager.addListener(new Logger());
initComponents();
layoutComponents();
}
现在你已经看到了添加新的事件、创建新的监听器--向您展示了事件驱动方案的灵活性和扩展性。你会发现随着你更多地开发事件集中的程序,你会更加娴熟地在你的应用中创建通用动作。像其它所有事情一样,这只需要时间和经验。看起来在事件模型上已经做了很多研究,但是你还是需要把它和其它替代方案相比较。考虑开发时间成本;最重要的,这是一次性成本。一旦你创建好了监听器模型和它们的动作,以后向你的应用中添加监听器将是小菜一蝶。
本文地址:http://com.8s8s.com/it/it15879.htm