ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

BlackMamba: A Swing Case Study
Pages: 1, 2, 3, 4, 5

Actions

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());
    }
}

Setup

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.

Summary

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:

  • A quick prototype will go a long way.
  • Identify and segregate the functionalities into separate layers. Craig Larman's Genernal Responsibility Assignment Software Patterns (GRASP) are helpful.
  • Choose the right packages to place classes in. Use OO principles like Dependency Inversion, Acyclic Dependencies, Inversion of Control, and Interface Segregation. Robert C Martin's paper on Design Principles and Design Patterns has a list of these.
  • Maximize code reuse by inheritance and not by copy-pasting.
  • Multi-threading is inevitable. It takes time to master.

Resources

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.