Running on Java 22-ea+27-2262 (Preview)
Home of The JavaSpecialists' Newsletter

074GoF Factory Method in Writing GUIs

Author: Dr. Heinz M. KabutzDate: 2003-07-14Java Version: 1.4Category: GUI
 

Abstract: Instead of using Rapid Application Development (RAD) tools for building GUIs, we can use GoF factory methods to create our user interfaces with code.

 

Welcome to the 74th edition of The Java(tm) Specialists' Newsletter. I have had complaints by readers that their SPAM filters think my newsletters are junk mail. Thanks to one of my readers, I have eliminated two characteristics of my newsletter that seem to trigger these false alerts. Please let me know if your SPAM filter still warns you - and if possible also tell me why it moans.

Switzerland was great! We had some excellent discussions around Java, and I met some very smart people indeed. It forced me to spend some more time reading up on the latest trends, one of which is Java Data Objects (JDO). With the latest release of JBoss 4, we now have both JDO and a reliable multicast based peer-to-peer JMS implementation. In addition to JBoss starting to ship with JDO, Jakarta now also have a project that includes JDO. To me, JDO is one of the most significant developments in the Java standards in a long time, even though it will be an optional standard in J2EE.

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

GoF Factory Method in Writing GUIs

Programmers often amazed me by claiming that Eclipse is not as good as XYZ IDE because it does not have a GUI editor. That seems to be the only weakness with Eclipse, so I am led to believe.

Seen from another angle, when I ask programmers which Design Patterns they know, I usually hear either Singleton, Factory or Facade. Singleton is usually an excuse to make a procedural design look more object-orientated. There is no pattern called Factory in the Gang-of-Four book. There is an Abstract Factory and a Factory Method, both of which are completely different to the "static method that creates objects" idiom. There is a lot of confusion about this, even in the Refactoring literature this is mixed up. Facade in the Gang-of-Four is in my estimation not really a Design Pattern, rather, it is an idiom that one can use when applying too many Design Patterns results in an overly complicated design. I have more arguments in my arsenal for these statements, but let us not argue. If you disagree, you can either join me on my Design Patterns Course, or we can just agree to disagree...

The Problem with RAD GUI Tools

Rabid Application Development. It promised to save millions of development dollars by replacing developers with wizards. However, in reality these wizards generate terrible, downright dangerous, code. We developers then spend more time improving it, than it would have taken, had we used our grey matter in the first place.

But that is not the worst problem with RAD GUI Tools. The worst problem is that they encourage the Copy & Paste Antipattern, where each time we develop a dialog, we start with a blank canvas. We do not think about reusing parts of the code, since the auto-generated code is difficult to work with. In addition, if we change too much of the code, we will not be able to work with the RAD GUI tool anymore. Usually all goes well until we have about 30 dialogs, and all of a sudden, the wheels come off this model. The code becomes too complicated to maintain, and eventually we move over to a web-based GUI so that we can start on a clean slate (and without RAD GUI tools!)

Let us look at an example, of some code that was written with evil wizards using an IDE that has been around for a long time. I have been a staunch supported of this unnamed IDE since version 1, and I can tell you that there have been almost no improvements in the GUI editor from version 3 to version 8 of the IDE. The IDE is great in general, but look at the code that is generated:

import javax.swing.UIManager;
import java.awt.*;

public class Application1 {
  boolean packFrame = false;

  //Construct the application
  public Application1() {
    Frame1 frame = new Frame1();
    //Validate frames that have preset sizes
    //Pack frames that have useful preferred size info, e.g. from their layout
    if (packFrame) {
      frame.pack();
    }
    else {
      frame.validate();
    }
    //Center the window
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Dimension frameSize = frame.getSize();
    if (frameSize.height > screenSize.height) {
      frameSize.height = screenSize.height;
    }
    if (frameSize.width > screenSize.width) {
      frameSize.width = screenSize.width;
    }
    frame.setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
    frame.setVisible(true);
  }
  //Main method
  public static void main(String[] args) {
    try {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    }
    catch(Exception e) {
      e.printStackTrace();
    }
    new Application1();
  }
}

And the frame that contains all the components looks like this:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class Frame1 extends JFrame {
  JPanel contentPane;
  BorderLayout borderLayout1 = new BorderLayout();
  JPanel jPanel1 = new JPanel();
  JLabel jLabel1 = new JLabel();
  JScrollPane jScrollPane1 = new JScrollPane();
  JPanel jPanel2 = new JPanel();
  JButton jButton1 = new JButton();
  JButton jButton2 = new JButton();
  JTable jTable1 = new JTable(new DefaultTableModel(100, 20) {
    public String getColumnName(int column) {
      return Integer.toString(column+1);
    }
    public Object getValueAt(int row, int column) {
      return Integer.toString((row+1)*(column+1));
    }
  });

  //Construct the frame
  public Frame1() {
    enableEvents(AWTEvent.WINDOW_EVENT_MASK);
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }
  //Component initialization
  private void jbInit() throws Exception  {
    contentPane = (JPanel) this.getContentPane();
    contentPane.setLayout(borderLayout1);
    this.setSize(new Dimension(603, 483));
    this.setTitle("Frame Title");
    jLabel1.setText("Multiplication Table");
    jButton1.setText("OK");
    jButton1.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseEntered(MouseEvent e) {
        jButton1_mouseEntered(e);
      }
      public void mouseExited(MouseEvent e) {
        jButton1_mouseExited(e);
      }
    });
    jButton2.setText("Help");
    jButton2.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        jButton2_mouseClicked(e);
      }
    });
    contentPane.add(jPanel1, BorderLayout.NORTH);
    jPanel1.add(jLabel1, null);
    contentPane.add(jScrollPane1, BorderLayout.CENTER);
    jScrollPane1.getViewport().add(jTable1, null);
    contentPane.add(jPanel2, BorderLayout.SOUTH);
    jPanel2.add(jButton1, null);
    jPanel2.add(jButton2, null);
  }
  //Overridden so we can exit when window is closed
  protected void processWindowEvent(WindowEvent e) {
    super.processWindowEvent(e);
    if (e.getID() == WindowEvent.WINDOW_CLOSING) {
      System.exit(0);
    }
  }

  void jButton1_mouseEntered(MouseEvent e) {
    jButton1.setEnabled(false);
  }

  void jButton1_mouseExited(MouseEvent e) {
    jButton1.setEnabled(true);
  }

  void jButton2_mouseClicked(MouseEvent e) {
    jButton2.setText("No Help");
  }
}

I have purposely left the code "as is", so that we can be reminded of the quality of code generated by GUI builders.

I will show you some classes that do the same, but take less code and are more readable. First, we extract the functionality to centre a window on the screen into a common class. Since this will be shared between other parts of our system, we should not count this towards the lines of code that we need.

import java.awt.*;

public class Windows {
  public static void centerOnScreen(Window window) {
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Dimension windowSize = window.getSize();
    windowSize.height = Math.min(windowSize.height, screenSize.height);
    windowSize.width = Math.min(windowSize.width, screenSize.width);
    window.setLocation((screenSize.width - windowSize.width) / 2, 
      (screenSize.height - windowSize.height) / 2);
  }
}

Then, we use the refactoring built into Eclipse to make the frame a bit more human editable:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class MultiplicationTable extends JFrame {
  private final JButton okButton = new JButton("OK");
  private final JButton helpButton = new JButton("Help");
  private final JTable table = new JTable(new DefaultTableModel(100, 20) {
    public String getColumnName(int column) {
      return Integer.toString(column+1);
    }
    public Object getValueAt(int row, int column) {
      return Integer.toString((row+1)*(column+1));
    }
  });

  //Construct the frame
  public MultiplicationTable() {
    super("Multiplication Table");
    setSize(603, 483);
    
    okButton.addMouseListener(new MouseAdapter() {
      public void mouseEntered(MouseEvent e) {
        okButton.setEnabled(false);
      }
      public void mouseExited(MouseEvent e) {
        okButton.setEnabled(true);
      }
    });
    helpButton.addMouseListener(new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        helpButton.setText("No Help");
      }
    });
    JPanel titlePanel = new JPanel();
    titlePanel.add(new JLabel("Multiplication Table"));
    getContentPane().add(titlePanel, BorderLayout.NORTH);
    getContentPane().add(new JScrollPane(table), BorderLayout.CENTER);
    JPanel buttonPanel = new JPanel();
    buttonPanel.add(okButton);
    buttonPanel.add(helpButton);
    getContentPane().add(buttonPanel, BorderLayout.SOUTH);
  }

  // instead of Application1, we have the following few lines
  public static void main(String[] args) {
    MultiplicationTable frame = new MultiplicationTable();
    Windows.centerOnScreen(frame);
    // instead of enabling the events and listening to window events:
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);    
    frame.show();
  }
}

This is better, in that the code has shrunk from 123 LOC down to just 54 lines. This is a simple example, with a complicated example, we would improve by even more. However, we have still not achieved very much reuse. We cannot use this frame in any other way except as a multiplication table.

Factory Method According To GoF

Seeing that you are subscribed to The Java(tm) Specialists' Newsletter, you are either a famous author of a Design Patterns book, or you have a copy of at least the Gang-of-Four book in your bookshelf. Yes? If not, I can highly recommend the book by Erich Gamma, et al [ISBN 0201633612] .

The book contains a Design Pattern called the Factory Method. Believe it or not, but that pattern took me the longest to grasp when I developed my course on Design Patterns. I can therefore recommend that you read the pattern in the book until you do not understand it anymore, and then read it a few more times until you understand it again. That process is called "being humbled" and it is the only way that I know in which a human can learn Design Patterns. Only once you realise that you do not understand, can you open up your mind to learn.

The intent of Factory Method according to the GoF book [ISBN 0201633612] is: "Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses."

Let us remember that when the book was written, Java had not been conceived and the examples were based on C++, which does not have interfaces. "Interface" therefore refers to the methods available in the class, it does not mean that the top-level class has to be an interface, a la Java.

The first step to make this example more extendable is to have creational methods for where we are creating objects:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class MultiplicationTable2 extends JFrame {
  public MultiplicationTable2(String title) {
    super(title);
    JPanel titlePanel = new JPanel();
    titlePanel.add(getDescription());
    getContentPane().add(titlePanel, BorderLayout.NORTH);

    JTable table = new JTable(makeTableModel());
    getContentPane().add(new JScrollPane(table), BorderLayout.CENTER);
    
    JPanel buttonPanel = new JPanel();
    JButton[] buttons = makeButtons();
    for (int i = 0; i < buttons.length; i++) {
      buttonPanel.add(buttons[i]);
    }
    getContentPane().add(buttonPanel, BorderLayout.SOUTH);
  }

  protected TableModel makeTableModel() {
    return new DefaultTableModel(100, 20) {
      public String getColumnName(int column) {
        return Integer.toString(column+1);
      }
      public Object getValueAt(int row, int column) {
        return Integer.toString((row+1)*(column+1));
      }
    };
  }

  protected JLabel getDescription() {
    return new JLabel("Multiplication Table");
  }
  protected JButton[] makeButtons() {
    final JButton okButton = new JButton("OK");
    final JButton helpButton = new JButton("Help");
    okButton.addMouseListener(new MouseAdapter() {
      public void mouseEntered(MouseEvent e) {
        okButton.setEnabled(false);
      }
      public void mouseExited(MouseEvent e) {
        okButton.setEnabled(true);
      }
    });
    helpButton.addMouseListener(new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        helpButton.setText("No Help");
      }
    });
    return new JButton[] { okButton, helpButton };
  }
  
  public static void main(String[] args) {
    MultiplicationTable2 frame = new MultiplicationTable2("Multiplication Table");
    // it is better to set the size outside of the frame construction
    frame.setSize(603, 483); 
    Windows.centerOnScreen(frame);
    // instead of enabling the events and listening to window events:
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);    
    frame.show();
  }
}

This refactoring exercise has actually increased the code by 12 lines.

Let us assume that we will have several such frames with tables and buttons in our system. We can write an AbstractTableFrame class that contains Factory Methods which we can subclass to make our MultiplicationTable3:

import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;

public abstract class AbstractTableFrame extends JFrame {
  public AbstractTableFrame(String title) {
    super(title);
    JPanel titlePanel = new JPanel();
    titlePanel.add(getDescription());
    getContentPane().add(titlePanel, BorderLayout.NORTH);

    JTable table = new JTable(makeTableModel());
    getContentPane().add(new JScrollPane(table), BorderLayout.CENTER);
    
    JPanel buttonPanel = new JPanel();
    JButton[] buttons = makeButtons();
    for (int i = 0; i < buttons.length; i++) {
      buttonPanel.add(buttons[i]);
    }
    getContentPane().add(buttonPanel, BorderLayout.SOUTH);
  }

  protected abstract TableModel makeTableModel();

  protected abstract JLabel getDescription();

  protected abstract JButton[] makeButtons();
}

We can now simply subclass this AbstractTableFrame and specify the Model and the Controller of the frame. The View is made by the AbstractTableFrame class. This separation of concerns is worth the effort that we have put into breaking up the original RAD generated code into these classes.

import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class MultiplicationTable3 extends AbstractTableFrame {
  public MultiplicationTable3() {
    super("Multiplication Table");
  }

  protected TableModel makeTableModel() {
    return new DefaultTableModel(100, 20) {
      public String getColumnName(int column) {
        return Integer.toString(column+1);
      }
      public Object getValueAt(int row, int column) {
        return Integer.toString((row+1)*(column+1));
      }
    };
  }

  protected JLabel getDescription() {
    return new JLabel("Multiplication Table");
  }
  protected JButton[] makeButtons() {
    final JButton okButton = new JButton("OK");
    final JButton helpButton = new JButton("Help");
    okButton.addMouseListener(new MouseAdapter() {
      public void mouseEntered(MouseEvent e) {
        okButton.setEnabled(false);
      }
      public void mouseExited(MouseEvent e) {
        okButton.setEnabled(true);
      }
    });
    helpButton.addMouseListener(new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        helpButton.setText("No Help");
      }
    });
    return new JButton[] { okButton, helpButton };
  }
  
  public static void main(String[] args) {
    MultiplicationTable3 frame = new MultiplicationTable3();
    frame.setSize(603, 483); 
    Windows.centerOnScreen(frame);
    // instead of enabling the events and listening to window events:
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);    
    frame.show();
  }
}

We do not achieve that much in terms of lines of code, since the MultiplicationTable3 class is only 3 lines shorter than MultiplicationTable. However, we have achieved the holy mantra of reusability, reusability, reusability. For example, let's write a new frame that contains a simple 3x4 table and one button:

import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class SimpleTableFrame extends AbstractTableFrame {
  public SimpleTableFrame() {
    super("Simple Table");
  }

  protected TableModel makeTableModel() {
    return new DefaultTableModel(3, 4);
  }

  protected JLabel getDescription() {
    return new JLabel("Empty Default Table");
  }

  protected JButton[] makeButtons() {
    return new JButton[] { new JButton(new AbstractAction("Close") {
      public void actionPerformed(ActionEvent e) {
        dispose();
      }
    })};
  }
  
  public static void main(String[] args) {
    SimpleTableFrame frame = new SimpleTableFrame();
    frame.setSize(603, 483); 
    Windows.centerOnScreen(frame);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);    
    frame.show();
  }
}

We have managed to write this frame in just 33 lines of code, and we could have done it with Emacs, vi, notepad, even edlin!

What Have We Achieved?

Reducing the number of lines is just one of the benefits of applying the Factory Method Design Pattern. In addition, we have now got a company-wide frame that we can reuse to represent any frame containing a table. What is more, say we would like to change the View of this frame, we only have to change one class, and all other frames will also look different. Very powerful stuff indeed.

My recommendation is that you write all of your Java GUIs like this and that you look for opportunities where you can apply the Factory Method in other code. In the long run, it will make your code more manageable and maintainable. Unless of course you are a highly-paid contractor who is paid per hour. Then rather use the RAD tools because your boss will think you are working "rapidly". I expect a cut from your next invoice for that tip ;-)

I have to thank the folks at jGuru who many years ago pointed me in this direction and who made me rethink my views on RAD GUI development in Java.

Kind regards

Heinz

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...