Amaze your developer friends with design patterns

Java Design Patterns 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.

Figure 1. Use the default layout manager for a panel

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.

Figure 1b. Use the Strategy pattern to change layout

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.)

Figure 2. Use the Composite pattern to nest AWT containers

The applet shown in Figure 2 is listed in Example 2:

1 2 Page 1
Page 1 of 2
How to choose a low-code development platform