|
The Java Specialists' Newsletter
Issue 074 2003-07-14
Category:
GUI
Java version: GoF Factory Method in writing GUIsby Dr. Heinz M. Kabutz
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.
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 .
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 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
GUI Articles
Related Java Course
Discuss at The Java Specialist Club
|