| Sign In/My Account | View Cart |
BlackMamba: A Swing Case Study
Pages: 1, 2, 3, 4, 5
It is very common to have widgets like buttons, popup menus, and toolbar menus to have the same functionality. BlackMamba has toolbar menus and buttons on one screen for Login, Logout, and Configuration. Instead of having ActionListener code strewn across classes, it can be written once, using the javax.swing.Action class. This class can be used for both menu items and buttons. This way, not only will the behavior be consistent, but also the look and feel. Both the menu items and the buttons will have the same mnemonics, accelerator keys, and same disabled/enabled state.
In BlackMamba, when the user has not logged into the server, Login is enabled and Logout is disabled, and vice-versa when the user has logged in. The logic to choose the mail server, connect to it, fetch mails, and log out is spread across several control classes. There can be a central location from which these Actions can be accessed to avoid passing it across screens.
public class Actions
{
public static final AbstractAction loginAction =
new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
...
...
}
};
public static final AbstractAction logoutAction =
new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
...
...
}
};
}
public class Start
{
...
protected void prepareMenus()
{
//Login
ActionPropsSetter.setActionProps(
Actions.loginAction, mambaFrm.loginMnuIt);
mambaFrm.loginMnuIt.setAction(Actions.loginAction);
mambaFrm.loginMnuIt.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
login();
}
});
//Logout
ActionPropsSetter.setActionProps(
Actions.logoutAction, mambaFrm.logoutMnuIt);
mambaFrm.logoutMnuIt.setAction(Actions.logoutAction);
mambaFrm.logoutMnuIt.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
Runnable runnable = new Runnable()
{
public void run()
{
logout();
}
};
SwingUtilities.invokeLater(runnable);
}
});
Actions.logoutAction.setEnabled(false);
}
}
public class Mails
{
...
protected void prepareButtons()
{
...
mailsPnl.logoutBtn.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
beforeLogout();
}
});
mailsPnl.logoutBtn.setAction(
Actions.logoutAction);
}
...
protected void beforeLogout()
{
...
Actions.loginAction.setEnabled(true);
Actions.logoutAction.setEnabled(false);
...
}
}
Technically, the ActionListener code should have been inside Actions.loginAction and Actions.logoutAction. But I added these Action classes as an afterthought. I had to write a small utility method to copy the text, mnemonics, and accelerator keys from my menu items and buttons that I had already created in NetBeans to the Action classes. When you add an Action class to a menu item or button, it overwrites the widget's settings. So plan your code in advance to avoid such hacks.
public class ActionPropsSetter
{
public static void setActionProps(
Action action, JMenuItem menuItem)
{
action.putValue(Action.NAME,
menuItem.getText());
action.putValue(Action.MNEMONIC_KEY,
new Integer(menuItem.getMnemonic()));
action.putValue(Action.ACCELERATOR_KEY,
menuItem.getAccelerator());
}
}
BlackMamba uses a lot of helper classes: text databases, POP3 mail protocol helpers, mail classifiers, mail processors, threadpool, common data-structures, etc. It would require a lot of spaghetti code to move these instances around the control classes. A Singleton is a good solution. But my intention was not to force only one instance of helper classes in the VM, but to have a central location like a registry where all the configuration settings and helper class instances could be retrieved. I could also have all my initialization code here, such as reading files, loading images, initializing the Swing look and feel, etc. The only Singleton here is the registry. Having just one single place from which all changes can be made also improves code clarity.
Another case against turning all the helper classes into Singletons is that it hinders the use of MockObjects for unit testing. For example if blackmamba.helpers.mail.MailHelper had to be replaced by a MockMailHelper that would just simulate a POP3 server's functions for unit testing, I would have to change all the classes that use MailHelper with MockMailHelper. Or I would have to send an instance of MockMailHelper in the static getInstance() method of MailHelper. Also, since the static getInstance() method on the Singleton is the only way to create and access the MailHelper, when a new sub-class has to be used instead of the original helper class for say, IMAP4 instead of POP3 this static method cannot be overridden to return IMAP4MailHelper.
To avoid all this, I write MailHelper like a regular class with a public constructor. My blackmamba.Setup class will have a public static reference to MailHelper. MockMailHelper can extend MailHelper and override all methods to simulate a POP3 server. A drawback of using a central registry instead of explicit Singletons is that there is nothing preventing the developer from creating multiple instances of the helper classes.
public class Setup
{
private static AccountsDatabase accountsDatabase;
...
private static LiteThreadPool liteThreadPool;
private static MailProcessor mailProcessor;
private static MailHelper mailHelper;
...
public static final boolean DEBUG = false;
public static final String DIR_NAME_RESOURCES =
"resources";
public static final String FILE_NAME_ACCOUNTS =
"AccountsDB.txt";
...
public static final int DEF_MAILS_SIZE =
15 * 100 * 1000;
...
public static final int FETCH_SIZE = 10;
private Setup()
{
}
...
public static void init() throws Exception
{
initDatabases();
//LiteThreadPool
liteThreadPool = new LiteThreadPool();
//MailProcessor
mailProcessor = new MailProcessor();
//MailHelper
mailHelper = new MailHelper();
}
protected static void initDatabases()
throws IOException
{
String accountsDatabaseFile = new File(
rootDir,
FILE_NAME_ACCOUNTS).getAbsolutePath();
...
//AccountsDatabase
accountsDatabase = new AccountsDatabase();
accountsDatabase.setFileName(
accountsDatabaseFile);
...
accountsDatabase.load();
...
spammersDatabase.load();
...
subjectsDatabase.load();
...
sizesDatabase.load();
}
public static void initLF()
{
...
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
...
}
public static AccountsDatabase
getAccountsDatabase()
{
return accountsDatabase;
}
...
...
public static LiteThreadPool getLiteThreadPool()
{
return liteThreadPool;
}
public static MailProcessor getMailProcessor()
{
return mailProcessor;
}
public static MailHelper getMailHelper()
{
return mailHelper;
}
...
}
Using a registry is not the ultimate solution. Figure 5 shows that the number of dependencies on Setup class is quite high. It will not scale well. It is susceptible to the same kind of problems that face the bloated controller class. There is a concept called Inversion of Control, which is really a geeky version of the Hollywood Principle: "Don't call us, we'll call you."
In this arrangement all important classes are turned into Components, which usually means implementing a few Lifecycle interfaces and registering these classes in a properties or XML file. These "Components" must not be confused with java.awt.Components. The Components here are managed by a lightweight container that reads the configuration/setup file, instantiates the Components, and supplies references to other Components and any configuration information through these Lifecycle methods.
A well-known example is the Java Servlet framework. All Servlets extend the HTTPServlet class. The Servlets are registered with the Web Server by filling up the web.xml file. Initialization parameters can be supplied in the web.xml file. The Servlets are created, initialized with environment variables and other start up parameters, used to service HTTP requests, and finally discarded by the web server. Servlets can invoke other Servlets in the web application by just forwarding requests to the URL on which the other Servlet is listening. Even the URL to be invoked can be parameterized using the web.xml file.
As you can see there is a considerable level of de-coupling between Components in this framework.
I hope by now you are able to fully appreciate the extent of careful planning that should go into developing an application. Notice that nowhere have I even mentioned the internal workings of BlackMamba. Things like how it fetches mails from the server; what kind of logic it uses to classify mails; or where the account information, list of blocked senders, address book, etc. are maintained. Just describing the aspects common to every application itself required a whole article.
The important points to remember are:
Ashwin Jayaprakash is a software engineer at BEA Systems R & D Centre, Bangalore, using Java/J2EE to develop WebLogic Integration products.
Return to ONJava.com.
Showing messages 1 through 3 of 3.
I didn't see any mention of one of the problems which I find
most tricky to deal with in Swing. You talk about how it's important
to use invokeLater (or other techniques) to assure that the UI is
only changed in the UI thread, however, you didn't seem to
acknowledge the effects this has on your CustomTableModel.
The way your custom table model is designed, addMail and removeMail
should only be called from the foreground thread, because they
immediately change the underlying table model data. It's not valid to
change the table model data outside of the UI thread, because the
JTable could be in the middle of an existing paint request.
This error is seldom seen in practice when adding elements to a table
model, because the JTable still thinks there are fewer items in the
list. The new item is simply ignored until it sees the
tablerowsinserted notification. However, this is critical when
removing rows, because the JTable could be in the middle of a paint.
Here is an example: When removing row "3", JTable might fetch
information for rows 3-6 during a paint. Since you already removed
row 3 in another thread, you would return what formerly was 4-7. Then
when you tell it row 3 is deleted, it removes the information for
what is actually row 4, and while it thinks that it has 4-6, it
actually has 5-7. The real row 4 is mysteriously gone, and when it
asks for row 7 to repaint a new bottom row on the screen, there will
be two copies of row 7.
This is not the only race condition but it is one of the more common
ones. I think you're writeup would be more complete if it made at
least some mention of these issues.
Thanks for the great article!
- David