The Solution: Event-Driven Programming
All of the previous solutions share the same fatal flaw -- trying to represent a functional set of tasks while continuously changing threads. But changing threads requires an asynchronous model, since threads process Runnables asynchronously. Part of the problem is that we are trying to implement a synchronous model of a series of functions on top of an asynchronous threading model. That is the reason for all of the chaining and dependencies between Runnables; the order of execution and inner-class scooping issues. If we can make this truly asynchronous, we can solve our problem and simplify Swing threading tremendously.
Before we go on, let's just enumerate the problems we are trying to solve:
- Execute code in the appropriate thread.
- Asynchronous execution using
SwingUtilities.invokeLater().
And asynchronous execution causes the following problems:
- Coupled components.
- Difficult variable passing.
- Order of execution.
Let's think for a minute about message-based systems like Java Messaging Service (JMS), since they promote loosely coupled components functioning in an asynchronous environment. Messaging systems fire asynchronous events into the system, as described at the Enterprise Integration Patterns site. Interested parties listen for that event and react to it -- usually by performing some work of their own. The result is a set of modular, loosely coupled components that can be added to and removed from the system without affecting the rest of the system. But more importantly, dependencies between components are minimized, since each component is well defined and encapsulated -- each responsible for its own work. They simply fire messages to which the other components respond, and respond to messages that have been fired.
For now, let's ignore the threading issue and work on decoupling and moving to an asynchronous environment. After we've solved the asynchronous problems, we'll go back and take a look at the threading issue. As we'll see, solving it at that point will be much easier.
Let's take our example from the first section and begin migrating it to an event-based model. To get started, let's abstract the lookup call into a class called LookupManager. This will enable us to move all of the database logic out of the UI class and will eventually allow us to completely decouple the two. Here is the code for the LookupManager class:
class LookupManager {
private String[] lookup(String text) {
String[] results = ...
// database lookup code
return results
}
}
Now we'll start to move towards an asynchronous model. To make this call asynchronous, we need to abstract the call from the return. In other words, methods can't return anything. We'll start by deciding what the relevant actions are that other classes might want to know about. The obvious event in our case is the completion of the search. So let's create a listener interface reflecting these actions. The interface will have a single method called lookupCompleted(). Here is the interface:
interface LookupListener {
public void lookupCompleted(Iterator results);
}
Following the Java standard, we'll create another class called LookupEvent to contain the result String array rather than passing the String array around directly. This will also allow us flexibility down the road to pass other information without changing the LookupListener interface. For example, we could include the search string along with the results. Here is the LookupEvent class:
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;
}
}
Notice that the LookupEvent class is immutable. This is important, since we are unaware who will be processing these events down the road. And unless we are willing to make a defensive copy of the event that we send to each listener, we need to make the event immutable. If not, a listener could unintentionally or maliciously modify the event and break the system.
Now we need to call the lookupComplete() event from LookupManager. We'll start by adding a collection of LookupListeners to LookupManager:
List listeners = new ArrayList();
And we'll add methods to add and remove LookupListeners from LookupManager:
public void addLookupListener(LookupListener listener){
listeners.add(listener);
}
public void removeLookupListener(LookupListener listener){
listeners.remove(listener);
}
We need to call the listeners from the code when the action occurs. In our example, we'll fire a lookupCompleted() event when the lookup returns. This means iterating through the list of listeners and calling their lookupCompleted() methods with a LookupEvent.
I like to extract this code to a separate method called fire[event-method-name] that constructs an event, iterates through the listeners, and calls the appropriate methods on the listeners. It helps to isolate the code for calling the listeners from the main logic. Here is our fireLookupCompleted method:
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);
}
}
The second line creates a new collection, passing it the collection of listeners from which to create the array. This is in case the listener decides to remove itself from the LookupManager as a result of the event. If we don't safely copy the collection, we'll get nasty errors where listeners are not called when they should be.
Next, we'll call the fireLookupCompleted() helper method from the point that the action is completed. In this case, it's the end of the lookup method when the results are returned. So we can change the lookup method to fire an event rather than return the String array itself. Here is the new lookup method:
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);
}
Now let's add our listener to LookupManager. We want to update the text area when the lookup returns. Previously, we just called the setText() method directly, since the text area was in local as the database calls were being done in the UI. Now that we've abstracted the lookup logic out from the UI, we'll make the UI class a listener to the LookupManager to listen for lookup events and update itself accordingly. First, we'll implement the listener in the class declaration:
public class FixedFrame implements LookupListener
Then we'll implement the interface method
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);
}
}
Finally, we'll register it as a listener to the LookupManager.
public FixedFrame() {
lookupManager = new LookupManager();
//here we register the listener
lookupManager.addListener(this);
initComponents();
layoutComponents();
}
For simplicity, I added it as a listener in the class constructor. This works fine for most systems. As systems get more complicated, you may want to refactor and abstract the listener registration out of constructors, allowing for greater flexibility and extensibility.
Now that you can see everything connected, notice the separation of responsibilities. The user interface class is responsible for the display of information -- and only the display of information. The LookupManager class, on the other hand, is responsible for all lookup connections and logic. Additionally, LookupManager is responsible for notifying listeners when it changes -- but not what it should do when those changes occur. This allows you to connect an arbitrary set of listeners.
To see how to add new events, let's go back and add an event for starting a lookup. We can add an event to our LookupListener called lookupStarted() that we will fire before the lookup is executed. Let's also create a fireLookupStarted() event calling lookupStarted() in all of the LookupListeners. Now the lookupMethod looks like this:
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);
}
And we'll add the new fire method, fireLookupStarted(). This method is identical to the fireLookupCompleted() method except that we are calling the lookupStarted() method on the listener, and that the event does not have a result set yet. Here is the code:
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);
}
}
And finally, we'll implement the lookupStarted() method in the UI that will set the text area to reflect the current search string.
public void lookupStarted(final LookupEvent e) {
outputTA.setText("Searching for: " +
e.getSearchText());
}
This example shows the ease of adding new events. Now, let's look at an example that shows the flexibility of the event-driven decoupling. We'll do this by creating a logger class that prints a statement out to the command line whenever a search is started or completed. We'll call the class Logger. Here is the code:
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());
}
}
Now, we'll add the Logger as a listener to the LookupManager in the FixedFrame constructor:
public FixedFrame() {
lookupManager = new LookupManager();
lookupManager.addListener(this);
lookupManager.addListener(new Logger());
initComponents();
layoutComponents();
}
Now you've seen examples of adding new events as well as creating new listeners -- showing you the flexibility and extensibility of the event-driven approach. You'll find that as you develop more with event-centered programs, you start to get a better feeling for creating generic actions that are used throughout your application. Like anything else, it just takes some time and experience. And it seems like a lot of work up front to set up the event model, but you have to weigh it against the consequences of other alternatives. Consider the development time cost; first of all, it's a one-time cost. Adding listeners later to your applications, once you set up your listener model and their actions, is trivial.
Threading
At this point, we've solved our stated asynchronous problems; decoupled components through listeners, variable passing through event objects, and order of execution through a combination of event generation and registered listeners. With that behind us, let's get back to the threading issue, since that's what brought us here in the first place. It's actually quite easy: since we have asynchronously functioning listeners, we can simply have the listeners themselves decide what thread they execute in. Think about the separation between the UI class and the LookupManager. The UI class is deciding what kind of processing to do, based on the event. Also, that class is all Swing, whereas a logging class would not be. So it makes a lot of sense to have the UI class be responsible for which thread it executes in.
So let's take a look at our UI class again. Here is the lookupCompleted() method without threading:
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);
}
}
We know that this is going to be called from a non-Swing thread, since the events are being fired directly from the LookupManager, which cannot be executing code in the Swing thread. Since all of the code is functioning asynchronously (we don't have to wait for the listener method to complete to invoke any other code), we can redirect the code into the Swing thread using SwingUtilities.invokeLater(). Here is the new method, passing an anonymous Runnable to 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);
}
}
}
);
}
If any LookupListeners are not executing in the Swing thread, we can execute in the listener code in the calling thread. As a rule of thumb, we want all of the listeners to be notified quickly. So if you have a listener that is going to take a lot of time to complete its functionality, you may want to create a new Thread or send the time consuming code off to a ThreadPool for execution.
The last step is to make the LookupManager perform the lookup in a non-Swing thread. Currently, the LookupManager is being called from a Swing thread in the JButton's ActionListener. Now we have a decision to make; either we can introduce a new thread in the JButton's ActionListener, or we could ensure that the lookup method itself guarantees that it is being executed in a non-Swing thread and starts a thread of its own. I prefer to manage Swing threading as close to the Swing classes as possible. This helps encapsulate all Swing logic together. If we added Swing threading logic to the LookupManager, we are introducing a level of dependency that is not necessary. Additionally, it is completely unnecessary for the LookupManager to spawn its own thread in a non-Swing context, such as a headless (non-graphical) user interface or, in our example, the Logger. Spawning new threads unnecessarily would only hurt your applications' performance, rather than help it. The lookup manager executes perfectly fine regardless of Swing threading -- so I like to keep the code out of there.
Now we need to make the JButton's ActionListener execute the lookup in a non-Swing thread. We'll create an anonymous Thread with an anonymous Runnable that executes the lookup.
private void searchButton_actionPerformed() {
new Thread(){
public void run() {
lookupManager.lookup(searchTF.getText());
}
}.start();
}
This completes our Swing threading. Simply adding the thread in actionPerformed() method and making sure the listeners are executing in the new thread takes care of the whole threading issue. Notice we didn't deal with any problems like the first examples. By spending our time defining an event-driven architecture, we save that time and more when it comes to Swing threading.
Conclusion
If you need to execute a lot of Swing code and non-Swing code in the same method, there is likely to be some code in the wrong place. The event-driven approach forces you to place code where it belongs -- and only where it belongs. If you have a method that is executing a database call and updating UI components in the same method, you have too much logic in one class. The process of going through and analyzing the events of your system and creating an underlying event model forces you to put code only where it belongs. Code for expensive database calls does not belong in UI classes; nor do UI component updates belong in non-UI classes. With an event-driven architecture, the UI is responsible for UI updates and some database manager is responsible for database calls. At that point, each encapsulated class worries about its own threading, with minimal concern about how the rest of the system is functioning. It certainly takes more effort up front to design and build an event-driven client, but over time, that up-front cost is far outweighed by the flexibility and maintainability of the resulting system.
Jonathan Simon is a developer and author specializing in user interaction.