Amaze your developer friends with design patterns
<strong>Java Design Patterns</strong> column kicks off with a look at three important design patterns
Welcome to the first installment of JavaWorld‘s Java Design Patterns. This column will explore, strictly from a Java perspective, many of the design patterns discussed in Design Patterns by Gamma, Helm, Johnson, and Vlissides — the so-called Gang of Four (GOF). Many regard that book, published by Addison-Wesley in 1994, as the seminal work regarding design patterns pertaining to object-oriented software development. Although it offers an excellent introduction to design patterns, all of the book’s code examples are written in C++ or Smalltalk, both of which Java developers can find difficult to translate. In this column I will present those patterns with examples written exclusively in Java, eliminating the need to translate from C++ and Smalltalk, and perhaps more importantly, to illustrate how you can take full advantage of Java programming language features when you use and implement design patterns. Additionally, I will discuss how design patterns are used and implemented in the Java 2 Software Development Kit (SDK).
In this introductory installment of Java Design Patterns, I discuss the nature of design patterns and what benefits they provide. I conclude with a brief tour of three patterns implemented in the Java 2 SDK: Strategy, Composite, and Decorator. That discussion should whet your appetite, giving you some idea of the benefits of design patterns.
Besides using Java to implement design patterns, this column will also use the Unified Modeling Language (UML) to portray static relationships between classes and dynamic interactions between objects at runtime. For those new to UML, please consult the citations listed in Resources at the end of this article.
Subsequent articles will explore a single design pattern. Each article will contain a sample design pattern implementation useable as a starting point for your own implementations. And, where applicable, each article will also discuss an implementation of those patterns in the Java 2 SDK to further illustrate how those patterns are used and what benefits they provide.
Finally, I would like this column to be as interactive as possible. Please feel free to send me your comments and questions. If I get enough feedback, I will begin each subsequent article in this column by discussing a few of your questions.
Audience
I’m targeting this column for experienced Java developers who are, for the most part, unfamiliar with design patterns. As a reader of this column, you should have a good grasp of Java programming language constructs. For example, you should understand inner classes, and when it might be appropriate to use them. On the other hand, you do not need to be a language lawyer to benefit from this column; for example, you don’t need to know, off the top of your head, the difference between nested and inner classes.
As a reader, you should also possess a minimal understanding of object-oriented design and development. For example, you should understand that classes encapsulate data and methods, and why encapsulation is important. You should also know what distinguishes object-oriented programming from programming with abstract data types (polymorphism), and why that distinction is important (because different kinds of objects with the same interface can be substituted for each other at runtime). If you don’t have that basic understanding, you can readily obtain it by consulting some of the references cited at the end of this article. As we go along, I will explain other object-oriented concepts such as why you should favor object composition over class inheritance (because inheritance breaks encapsulation).
What are design patterns?
In any field of endeavor, experience is always the best teacher. For example, if you’re learning a foreign language, it’s best to learn the way you learned your native language as a child — by immersion — which gives you the daily opportunity to listen to, and engage in, conversation with native speakers. That experience always proves more valuable than anything learned from a book.
Design patterns, simply put, capture the best practices of experienced object-oriented software developers. Design patterns are solutions to general software development problems. Those solutions were obtained by trial and error by numerous software developers over a substantial period of time. In the GOF book, those best practices are catalogued with 23 design patterns. The authors describe each design pattern with a name and an intent, which describes what the pattern does and what design issues or problems it addresses. Design pattern descriptions also include the applicability of a pattern, its structure, its participants (meaning classes, interfaces, objects, and so on), and the cooperation between those participants. Finally, each design pattern includes a sample implementation, known uses, and a discussion of related patterns.
Developers find design patterns important for a number of reasons. First, they give novice developers access to the best practices of more experienced developers. Second, they allow developers to think of their designs at higher levels of abstraction; for example, instead of focusing on low-level details, such as how to use inheritance, you can approach complex systems as a collection of design patterns that already make the best use of inheritance. That shift of focus to a higher level of abstraction also provides a common vocabulary when developers discuss design. Nowadays, for example, it’s quite common to hear developers discuss the pros and cons of a particular design pattern in a specific context. Finally, object-oriented software is designed and implemented in an iterative fashion, where among other things, code specific to a particular problem domain is refactored into more general-use code. Thus, refactoring results in software that is more resilient to change and therefore more reusable. Because design patterns evolved from numerous refactorings, you don’t have to refactor them yourself. And if you weren’t aware of design patterns when you initially implemented your software, you can use those patterns as targets for your refactoring.
A few examples of design patterns in the Java SDK
The Java 2 SDK contains many design pattern implementations. Some correspond directly to those discussed in the GOF book, others vary slightly from those patterns, and some have not been formally documented anywhere. Throughout this column I will limit our discussions of Java 2 design pattern implementations to those that closely approximate the patterns discussed in the GOF book.
In this section I briefly discuss three of the simplest and most widely used design patterns implemented in the Java 2 SDK: Strategy, Composite, and Decorator. For now, to give you a feel for design patterns, how they are used, and the benefits they provide, I briefly introduce those patterns and show how you can use them. In subsequent installments of this column, I will present more in-depth discussions of those patterns, including implementation details.
The Strategy pattern
Java’s Abstract Window Toolkit (AWT) provides implementations of common user interface components such as buttons, menus, scrollbars, and lists. Those components are laid out — meaning sized and positioned — inside containers such as panels, dialog boxes, and windows. But AWT containers do not perform the actual layout; instead, those containers delegate layout functionality to another object known as a layout manager. That delegation is an example of the Strategy design pattern.
The Strategy design pattern encapsulates a family of algorithms, or strategies, by implementing each algorithm in a different class. Clients that employ those algorithms delegate functionality to objects instantiated from those classes. That encapsulation allows clients to vary algorithms by delegating to different objects.
For the AWT, the clients are containers and the family of algorithms are layout algorithms encapsulated in layout managers. If a particular layout algorithm other than the default algorithm is required for a specific container, an appropriate layout manager is instantiated and plugged into that container. In this way, layout algorithms can vary independently from the containers that use them — one of the hallmarks of the Strategy design pattern: The Strategy design pattern allows algorithms to vary without having to change the clients that use those algorithms.
Figure 1.a shows a simple AWT application that opens a window with six buttons. The application contains an AWT Panel contained inside an AWT Frame. The panel contains six buttons and uses its default layout manager to position and size them. The default layout manager for panels is an instance of FlowLayout
, which sizes components according to their preferred sizes and positions them from left to right and top to bottom, centered in their container. You’ll find the application listed in Example 1.
Example 1. Use the Strategy pattern
import java.awt.Button;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.Panel;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class Test {
public static void main(String args[]) {
Panel panel = new Panel();
// Uncomment the following line of code to plug an
// instance of GridLayout into this application's panel.
// That layout manager will then be used to layout the
// application's buttons instead of the default
// FlowLayout layout manager.
// panel.setLayout(new GridLayout(3, 2));
panel.add(new Button(" 1 "));
panel.add(new Button(" 2 "));
panel.add(new Button(" 3 "));
panel.add(new Button(" 4 "));
panel.add(new Button(" 5 "));
panel.add(new Button(" 6 "));
Frame frame = new Frame("Using Layout Managers");
frame.add(panel);
frame.setSize(300, 150);
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
frame.show();
}
}
If the commented line of code in Example 1 is uncommented, the layout manager for the application’s panel will be set to an instance of GridLayout
, which lays out components in a grid and sizes them equally so that the components collectively fill the container in which they reside. Figure 1b shows the result of uncommenting that line of code.
To modify its layout algorithm, notice that you need not make code changes to an AWT container class, such as Panel
. That’s because the Strategy pattern encapsulates the concept that varies — in this case layout algorithms — which is one of the fundamental tenets of object-oriented design, and is a recurring theme among design patterns.
The Composite pattern
As mentioned above, the AWT implements components and containers. Components can be added to a container, as illustrated by the code listed in Example 1, by using the Component add(Component)
method defined in java.awt.Container
.
To facilitate complex user interface screens, user interface toolkits must allow nested containers, effectively composing components and containers into a tree structure. Additionally, it’s crucial for components and containers in that tree structure to be treated uniformly, without having to distinguish between them. For example, when the AWT determines the preferred size of a complex layout containing nested components and containers, it walks the tree structure and asks each component and container for its preferred size. If that traversal of the tree structure required distinction between components and containers, it would unnecessarily complicate that code, making it harder to understand, modify, extend, and maintain.
The AWT accomplishes nesting containers and uniform treatment of components and containers by implementing the Composite pattern. The Composite pattern dictates that containers are components, typically with an abstract class that represents both. In the AWT, that abstract class is java.awt.Component
, the superclass of java.awt.Container
; therefore, an AWT container can be passed to the add(Component)
method from java.awt.Container
because containers are components.
Figure 2 shows an applet that, by nesting containers, takes advantage of the AWT’s Composite pattern implementation. (Note: If you download the code to this article, use the JDK’s appletviewer
to test the applet listed in Example 2 to avoid incompatibilities in your browser’s JVM.)
The applet shown in Figure 2 is listed in Example 2:
Example 2. Use the Composite pattern
import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Panel;
import java.awt.Label;
import java.awt.TextField;
public class Test extends Applet {
public void init() {
Panel center = new Panel();
WorkPanel workPanel = new WorkPanel(center);
workPanel.addButton("Ok");
workPanel.addButton("Cancel");
center.add(new Label("Name:"));
center.add(new TextField(25));
setLayout(new BorderLayout());
add(workPanel);
}
}
class WorkPanel extends Panel {
private Panel buttonPanel = new Panel();
public WorkPanel(Panel centerPanel) {
setLayout(new BorderLayout());
add(centerPanel, "Center");
add(buttonPanel, "South");
}
public void addButton(String label) {
buttonPanel.add(new Button(label));
}
}
The applet listed in Example 2 contains a panel — an instance of WorkPanel
— that contains two other panels, as shown in Figure 3.
Because the AWT implements components and containers using the Composite pattern, Java applets and applications, like the one listed in Example 2, can easily nest components and containers in a tree structure. Additionally, components and containers can be treated uniformly.
The Decorator pattern
The java.io
package provides, among other things, a set of classes for reading input streams. Known as readers, each of those classes has a name that follows the pattern: xxxReader
.
Readers provide specialized functionality; for example, one reader reads from a file, another tracks line numbers, and yet another pushes characters back on the input stream. In all, eight different readers exist to read input streams.
It’s often necessary to combine the capabilities offered by java.io
readers; for example, you might want to read from a file, keep track of line numbers, and push certain characters back on the input stream, all at the same time. The java.io
package’s designers could have used inheritance to provide a wide array of such reader combinations; for example, a FileReader
class could have a LineNumberFileReader
subclass, which could in turn have a PushBackLineNumberFileReader
subclass. But using inheritance to compose the most widely used combinations of reader functionality would result in a veritable explosion of classes. So how did the java.io
package’s designers create a design that allows you to combine reader functionality in any way you desire with only 10 reader classes? As you might guess, they used the Decorator pattern.
Instead of using inheritance to add functionality to classes at compile time, the Decorator pattern lets you add functionality to individual objects at runtime. That is accomplished by enclosing an object in another object. The enclosing object forwards method calls to the enclosed object and typically adds some functionality of its own before or after forwarding. The enclosing object — known as a decorator — conforms to the interface of the object it encloses, allowing the decorator to be used as though it were an instance of the object it encloses. That may sound complicated, but using decorators is actually quite simple. For example, the code listed in Example 3 uses a decorator to read and print the contents of a file. The code also prints line numbers and transforms “Tab” characters to three spaces.
Example 3. Use the Decorator pattern
import java.io.FileReader;
import java.io.LineNumberReader;
public class Test {
public static void main(String args[]) {
if(args.length < 1) {
System.err.println("Usage: " + "java Test filename");
System.exit(1);
}
new Test(args[0]);
}
public Test(String filename) {
try {
FileReader frdr = new FileReader(filename);
LineNumberReader lrdr = new LineNumberReader(frdr);
for(String line; (line = lrdr.readLine()) != null;) {
System.out.print(lrdr.getLineNumber() + ":t");
printLine(line);
}
}
catch(java.io.FileNotFoundException fnfx) {
fnfx.printStackTrace();
}
catch(java.io.IOException iox) {
iox.printStackTrace();
}
}
private void printLine(String s) {
for(int c, i=0; i < s.length(); ++i) {
c = s.charAt(i);
if(c == 't') System.out.print(" ");
else System.out.print((char)c);
}
System.out.println();
}
}
If you use the code listed in Example 3 to read itself, you will get output that looks like Example 4.
Example 4. Output from using the application in Example 3 to print itself
1: import java.io.FileReader;
2: import java.io.LineNumberReader;
3:
4: public class Test {
5: public static void main(String args[]) {
6: if(args.length < 1) {
7: System.err.println("Usage: " + "java Test filename");
8: System.exit(1);
9: }
10: new Test(args[0]);
11: }
12: public Test(String filename) {
13: try {
14: FileReader frdr = new FileReader(filename);
15: LineNumberReader lrdr = new LineNumberReader(frdr);
16:
17: for(String line; (line = lrdr.readLine()) != null;) {
18: System.out.print(lrdr.getLineNumber() + ":t");
19: printLine(line);
20: }
21: }
22: catch(java.io.FileNotFoundException fnfx) {
23: fnfx.printStackTrace();
24: }
25: catch(java.io.IOException iox) {
26: iox.printStackTrace();
27: }
28: }
29: private void printLine(String s) {
30: for(int c, i=0; i < s.length(); ++i) {
31: c = s.charAt(i);
32:
33: if(c == 't') System.out.print(" ");
34: else System.out.print((char)c);
35: }
36: System.out.println();
37: }
38: }
Notice how I’ve constructed the line reader used in Example 3:
FileReader frdr = new FileReader(filename);
LineNumberReader lrdr = new LineNumberReader(frdr);
The LineNumberReader
decorator encloses another reader; in this case, the enclosed reader is an instance of FileReader
. The line number reader forwards method calls, such as read()
, to its enclosed reader and tracks line numbers, which can be accessed with LineNumberReader.getLineNumber()
. Because LineNumberReader
is a decorator, you can easily track line numbers for any type of reader.
More to come
In this article I provided an overview of design patterns, but in the process I may have created as many questions as I’ve answered. For example, although I demonstrated how to use three popular design patterns, I did not show you how to implement those patterns. In subsequent articles, I will discuss many of the design patterns from the GOF book in detail, including the best uses for those patterns, and how they are used and implemented.
Homework
If you look at the reader classes in java.io
, you will find another decorator: BufferedReader
. That class buffers reads, making it more efficient than an unbuffered reader. In light of this new discovery, you might decide to make Example 3 more efficient, like this:
FileReader frdr = new FileReader(filename);
BufferedReader brdr = new BufferedReader(frdr); // "mix in" a buffered reader
LineNumberReader lrdr = new LineNumberReader(brdr);
Answer the following questions:
- Will the preceding code fragment compile?
- How can you add two lines of code to the main method to see if the example runs faster?
- Will the modified example run faster? Why or why not?