原文: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();
}
现在你已经看到了添加新的事件、创建新的监听器--向您展示了事件驱动方案的灵活性和扩展性。你会发现随着你更多地开发事件集中的程序,你会更加娴熟地在你的应用中创建通用动作。像其它所有事情一样,这只需要时间和经验。看起来在事件模型上已经做了很多研究,但是你还是需要把它和其它替代方案相比较。考虑开发时间成本;最重要的,这是一次性成本。一旦你创建好了监听器模型和它们的动作,以后向你的应用中添加监听器将是小菜一蝶。
线程
到现在,我们已经解决了上面的异步问题;通过监听器使组件脱耦,通过事件对象传递变量,通过事件产生和监听器的注册的组合决定执行的顺序。让我们回到线程问题,因为正是它把我们带到了这儿。实际上非常容易:因为我们已经有了异步功能的监听器,我们可以简单地让监听器自己决定它们应该在哪个线程中执行。考虑UI类和LookupManager的分离。UI类基于事件,决定需要什么处理。并且,该类也是Swing,而日志类不是。所以让UI类负责决定它应该在什么线程中执行将更加有意义。所以,让我们再次看看UI类。下面是没有线程的lookupCompleted()方法:
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);
}
}
我们知道这将在非Swing线程中调用,因为该事件是直接在LookupManager中触发的,这将不是在Swing线程中执行。因为所有的代码功能上都是异步的(我们不必等待监听器方法允许结束后才调用其它代码),我们可以通过SwingUtilities.invokeLater()将这些代码改道到Swing线程。下面是新的方法,传入一个匿名Runnable到SwingUtilities.invokeLater():
public void lookupCompleted(final LookupEvent e) {
//notice the threading
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
outputTA.setText("");
String[] results = e.getResults();
for (int i = 0;
i < results.length;
i++) {
String result = results[i];
outputTA.setText(outputTA.getText() +
"\n" + result);
}
}
}
);
}
如果任何LookupListener不是在Swing线程中执行,我们可以在调用线程中执行监听器代码。作为一个原则,我们希望所有的监听器都迅速地接到通知。所以,如果你有一个监听器需要很多时间来处理自己的功能,你应该创建一个新的线程或者把耗时代码放入ThreadPool中等待执行。
最后的步骤是让LookupManager在非Swing线程中执行lookup。当前,LookupManager是在JButton的ActionListener的Swing线程中被调用的。现在是我们做出决定的时候,或者我们在JButton的ActionListener中引入一个新的线程,或者我们可以保证lookup自己在非Swing线程中执行,自己开始一个新的线程。我选择尽可能和Swing类贴近地管理Swing线程。这有助于把所有Swing逻辑封装在一起。如果我们把Swing线程逻辑添加到LookupManager,我们将引入了一层不必要的依赖。并且,对于LookupManager在非Swing线程环境中孵化自己的线程是完全没有必要的,比如一个非绘图的用户界面,在我们的例子中,就是Logger。产生不必要的新线程将损害到你应用的性能,而不是提高性能。LookupManager执行的很好,不管Swing线程与否--所以,我喜欢把代码集中在那儿。
现在我们需要将JButton的ActionListener执行lookup的代码放在一个非Swing线程中。我们创建一个匿名的Thread,使用一个匿名的Runnable执行这个lookup。
private void searchButton_actionPerformed() {
new Thread(){
public void run() {
lookupManager.lookup(searchTF.getText());
}
}.start();
}
这就完成了我们的Swing线程。简单地在actionPerformed()方法中添加线程,确保监听器在新的线程中执行照顾到了整个线程问题。注意,我们不用处理像第一个例子那样的任何问题。通过把时间花费在定义一个事件驱动的体系,我们在和Swing线程相关处理上节约了更多的时间。
结论
如果你需要在同一个方法中执行大量的Swing代码和非Swing代码,很容易将某些代码放错位置。事件驱动的方式将迫使你将代码放在它应该在的地方--它仅应该在的地方。如果你在同一个方法中执行数据库调用和更新UI组件,那么你就在一个类中写入了太多的逻辑。分析你系统中的事件,创建底层的事件模型将迫使你将代码放到正确的地方。将费时的数据库调用代码放在非UI类中,也不要在非UI组件中更新UI组件。采用事件驱动的体系,UI负责UI更新,数据库管理类负责数据库调用。在这一点上,每一个封装的类都只用关心自己的线程,不用担心系统其它部分如何动作。当然,设计、构建一个事件驱动的客户端也很有用,但是需要花费的时间代价远超过带来的结果系统的灵活性和可维护性的提高。