Strategy for success

The powerful Strategy design pattern aids object-oriented design

Back in 1984, I graduated from college with a mechanical engineering degree and went to work as a software engineer. After teaching myself C programming, by 1985 I was busily developing a 50,000-line graphical user interface (GUI) for Unix. That was fun.

By the end of 1985, my program complete, I was ready to move to other projects—or so I thought. I soon received a slew of bug reports and enhancement requests, so I started wading through my 50,000 lines to make fixes. That was hard.

I had created a veritable house of cards that came tumbling down almost daily. And that house of cards fell easily; even the most innocuous changes often left me debugging for hours just to restore the program's stability.

I thought I had stumbled upon an important software development tenet: have fun during development, then look for a new job after deployment. In fact, however, my difficulties stemmed from my ignorance of the most fundamental tenet of object-oriented (OO) software development: encapsulation. My program was a huge switch-statement collection that invoked different functions under different circumstances—resulting in tightly coupled and difficult-to-change software.

In this Java Design Patterns installment, I discuss perhaps the most fundamental design pattern: Strategy. If I had known about the Strategy pattern in 1984, I would have avoided a great deal of work.

The Strategy pattern

In Chapter 1 of the Gang of Four's (GOF) Design Patterns, the authors discuss several OO design principles comprising the core of many patterns. The Strategy pattern embodies two such principles—encapsulate the concept that varies and program to an interface, not an implementation. The Design Patterns authors define the Strategy pattern as:

Define a family of algorithms, encapsulate each one, and make them interchangeable. [The] Strategy [pattern] lets the algorithm vary independently from clients that use it.

The Strategy pattern lets you build software as a loosely coupled collection of interchangeable parts, in contrast to a monolithic, tightly coupled system. That loose coupling makes your software much more extensible, maintainable, and reusable.

To teach the Strategy pattern, I first illustrate how Swing uses the Strategy pattern to draw borders around its components. Then I discuss how Swing benefits by using the Strategy pattern, and finally I explain how you can implement the Strategy pattern in your own software.

Swing borders

You can draw borders around almost all Swing components, including panels, buttons, lists, and so on. Swing provides numerous border types for its components: bevel, etched, line, titled, and even compound. Borders for Swing components are drawn by the JComponent class, which acts as the base class for all Swing components by implementing functionality common to all Swing components.

JComponent implements paintBorder(), a method for painting borders around components. Swing's creators could have implemented paintBorder() like the method in Example 1:

Example 1. The wrong way to paint Swing borders

// A hypothetical JComponent.paintBorder method
protected void paintBorder(Graphics g) {
   switch(getBorderType()) {
      case LINE_BORDER:   paintLineBorder(g);
                          break;
      case ETCHED_BORDER: paintEtchedBorder(g);
                          break;
      case TITLED_BORDER: paintTitledBorder(g);
                          break;
      ...
   }
}

Example 1's hypothetical JComponent.paintBorder() method hard codes border painting in JComponent, which tightly couples that functionality and the JComponent class.

You can see the consequences if you tried to implement a new border type—you'd have to modify JComponent in at least three places: First, you would have to add a new integer value corresponding to your new border type. Second, you would have to add a case to the switch statement. And third, you would have to implement a paintXXXBorder() method, where XXX designates the border type.

Obviously, you won't have much success extending the preceding paintBorder() implementation. Not only would you find it difficult to extend paintBorder() with a new border type, but the JComponent class is not yours to modify in the first place—it's part of the Swing toolkit, meaning you would have to recompile the class and rebuild the entire toolkit. You would also have to require your users to use your renegade Swing version instead of the standard version, and you'd have work to do with the next Swing release. Also, because you've added new border-painting functionality to the JComponent class, every Swing component can now access that functionality whether you like it or not—you cannot restrict your new border to a particular component type.

Finally, Swing components would not be extensible if the JComponent class implemented its functionality with switch statements, as in Example 1.

What is the OO alternative? Decouple, using the Strategy pattern, JComponent from the code that paints borders, so you can vary the border painting algorithm without modifying the JComponent class. By employing the Strategy pattern, you encapsulate the concept that varies, in this case painting a border, and program to an interface, not an implementation by providing a Border interface. Let's see how the JComponent uses the Strategy pattern to paint borders. Example 2 lists the JComponent.paintBorder() method:

Example 2. The right way to paint Swing borders

// The actual implementation of the JComponent.paintBorder() method
protected void paintBorder(Graphics g) {
   Border border = getBorder();
   if (border != null) {
      border.paintBorder(this, g, 0, 0, getWidth(), getHeight());
   }
}

The preceding paintBorder() method paints a component's border with a border object. Under this scheme, the border object, not the JComponent class, encapsulates the border painting algorithm.

Notice that JComponent passes a reference to itself (the this argument) to Border.paintBorder() in case a border must retrieve information about its component, a technique known as delegation. With delegation, one object delegates functionality to another object, passing a reference to itself (see "Take Control with the Proxy Design Pattern" (JavaWorld, February 2002)).

The JComponent class references a border, which is returned from the JComponent.getBorder() method, shown in Example 3 with its corresponding setter method:

Example 3. Setter and getter methods for a Swing component's border

...
private Border border;
...
public void setBorder(Border border) {
   Border oldBorder = this.border;
   this.border = border;
   firePropertyChange("border", oldBorder, border);
   if (border != oldBorder) {
      if (border == null || oldBorder == null || !(border.getBorderInsets(this).
                                    equals(oldBorder.getBorderInsets(this)))) {
         revalidate();
      }       
      repaint();
   }
}
...
public Border getBorder() {
   return border;
}

When you set a component's border with JComponent.setBorder(), the JComponent class fires a property change event, and if the new border differs sufficiently from the old border, the component repaints. The getBorder() method simply returns the Border reference.

Figure 1's class diagram illustrates the relationship between borders and the JComponent class.

Figure 1. Swing borders. Click on thumbnail to view full-size image.

The JComponent class maintains a private reference to a Border object. Notice that because Border is an interface, not a class, the Swing components can have any border that implements the Border interface. (That's what it means to program to an interface, not an implementation.)

Now that you've seen how JComponent implements the Strategy pattern to paint component borders, let's test the implementation's extensibility by creating a new border type.

Create a new border type

Figure 2 shows a Swing application with three panels. I fitted each panel with a custom border, each a HandleBorder instance. Drawing programs frequently use handle borders to move and resize objects.

Figure 2. A handle border

Example 4 lists the HandleBorder class:

Example 4. The HandleBorder class

import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
public class HandleBorder extends AbstractBorder {
   protected Color lineColor;
   protected int thick;
   public HandleBorder() {
      this(Color.black, 6);
   }
   public HandleBorder(Color lineColor, int thick) {
      this.lineColor = lineColor;
      this.thick = thick;
   }
   public void paintBorder(Component component, 
                                  Graphics g, int x, int y, int w, int h) {
      Graphics copy = g.create();
      if(copy != null) {
         try {
            copy.translate(x,y);
            paintRectangle(component,copy,w,h);
            paintHandles(component,copy,w,h);
         }
         finally {
            copy.dispose();
         }
      }
   }
   public Insets getBorderInsets() {
      return new Insets(thick,thick,thick,thick);
   }
   protected void paintRectangle(Component c, Graphics g,
                           int w, int h) {
      g.setColor(lineColor);
      g.drawRect(thick/2,thick/2,w-thick-1,h-thick-1);
   }
   protected void paintHandles(Component c, Graphics g,
                           int w, int h) {
      g.setColor(lineColor);
      g.fillRect(0,0,thick,thick); // upper left
      g.fillRect(w-thick,0,thick,thick); // upper right
      g.fillRect(0,h-thick,thick,thick); // lower left
      g.fillRect(w-thick,h-thick,thick,thick); // lower right
      g.fillRect(w/2-thick/2,0,thick,thick); // mid top
      g.fillRect(0,h/2-thick/2,thick,thick); // mid left
      g.fillRect(w/2-thick/2,h-thick,thick,thick); // mid bottom
      g.fillRect(w-thick,h/2-thick/2,thick,thick); // mid right
   }   
}

The HandleBorder class extends javax.swing.border.AbstractBorder and overrides paintBorder() and getBorderInsets(). While HandleBorder's implementation isn't important, it is important that we can easily create new borders types because Swing uses the Strategy pattern to draw component borders.

Example 5 lists the Swing application shown in Figure 2:

Example 5. Use handle borders

import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;
public class Test extends JFrame {
   public static void main(String[] args) {
      JFrame frame = new Test();
      frame.setBounds(100, 100, 500, 200);
      frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
      frame.show();
   }
   public Test() {
      super("Creating a New Border Type");
      Container contentPane = getContentPane();
      JPanel[] panels = { new JPanel(), 
                     new JPanel(), new JPanel() };
      Border[] borders = { new HandleBorder(),
                     new HandleBorder(Color.red, 8),
                     new HandleBorder(Color.blue, 10) };
      contentPane.setLayout(
               new FlowLayout(FlowLayout.CENTER,20,20));
      for(int i=0; i < panels.length; ++i) {
         panels[i].setPreferredSize(new Dimension(100,100));
         panels[i].setBorder(borders[i]);
         contentPane.add(panels[i]);
      }
   }
}

The preceding application creates three panels (instances of javax.swing.JPanel) and three borders (instances of HandleBorder). Notice how you can easily specify borders for those panels by calling JComponent.setBorder().

Recall from Example 2 that a reference to a component passed to the component's border when JComponent calls Border.paintBorder()—a form of delegation. As I mentioned earlier, developers frequently use delegation with the Strategy pattern. The HandleBorder class does not use that component reference, but some borders use that reference to obtain information from their component. For example, Example 6 lists the paintBorder() method for such a border, the javax.swing.border.EtchedBorder:

Example 6. Some Swing borders retrieve information from their components

// The following listing is from
// javax.swing.border.EtchedBorder
public void paintBorder(Component component, Graphics g, int x, int y, 
                         int width, int height) {
   int w = width;
   int h = height;
   
   g.translate(x, y);
   
   g.setColor(etchType == LOWERED? getShadowColor(component) : getHighlightColor(component));
   g.drawRect(0, 0, w-2, h-2);
   
   g.setColor(etchType == LOWERED? getHighlightColor(component) : getShadowColor(component));
   g.drawLine(1, h-3, 1, 1);
   g.drawLine(1, 1, w-3, 1);
   
   g.drawLine(0, h-1, w-1, h-1);
   g.drawLine(w-1, h-1, w-1, 0);
   
   g.translate(-x, -y);
}

The javax.swing.border.EtchedBorder.paintBorder() method uses its component reference to find its component's shadow and highlight colors.

Implement the Strategy pattern

The Strategy pattern, as one of the simpler design patterns, proves easy to implement in your own software:

  1. Implement a Strategy interface for your strategy objects
  2. Implement ConcreteStrategy classes that implement the Strategy interface, as appropriate
  3. In your Context class, maintain a private reference to a Strategy object.
  4. In your Context class, implement public setter and getter methods for the Strategy object

The Strategy interface defines your Strategy objects' behavior; for example, the Strategy interface for Swing borders is the javax.swing.Border interface.

The concrete ConcreteStrategy classes implement the Strategy interface; for example, for Swing borders, the LineBorder and EtchedBorder classes are ConcreteStrategy classes.

1 2 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more