Three approaches for decorating your code

Which implementation of the Decorator pattern is right for your code?

Everybody who reads the famous book Design Patterns knows the Decorator pattern. Let's, for a moment, forget what we know about this concept and try to understand Decorator from our everyday experience.

A decorator is something or somebody who decorates something or (maybe) somebody. A decorator slightly changes the decorated object, yet doesn't change its nature. Examples: A frame decorates the picture. Makeup decorates a woman's face. A husband decorates his wife and vise versa.

Let's discuss some examples from our specialty, computer science:

  1. The JScrollPane decorates portions of JComponent (a.k.a. View). In this case, the nature of JComponent doesn't change, but a new property (scrollable) is added.
  2. BufferedInputStream is a decorator of InputStream. BufferedInputStream is an InputStream, but works more quickly, because it uses an inside buffer.
  3. Let's consider DebugButton, which is the same as a JButton, but writes a message to the log file when a button is pressed. DebugButton is a decorator of JButton, because it slightly changes JButton but doesn't change its nature.
  4. Let's consider ScrollOverButton, which adds the scroll-over behavior to JButton. Adding scroll-over makes the button look flat when the cursor is out of the button frame, otherwise the button is prominent. Evidently, ScrollOverButton is a decorator of JButton.

Now, let's consider Decorator's implementation issues. There are three types of implementations:

  1. Inheritance
  2. Wrapper
  3. External

In this article, I discuss these implementation types and their pros and cons.

Inheritance

A generic and simple idea first occurs to programmers when thinking about decorators: write the class, which extends the decorated class. The additional behavior can be implemented as the new methods or as the changing implementation of existing methods:

public class DebugButton extends JButton
{
   public DebugButton()
   {
      addActionListener(new ActionListener()
      {
         System.out.println("debug message");
      });
   }
}

In the same way, we can implement ScrollOverButton: instead of ActionListener, we can add MouseListener. MouseListener callbacks (methods) will change the JButton border from EmptyBorder to RaisedBevelBorder, when mouseEntered() is called, and from RaisedBevelBorder to EmptyBorder, when mouseExited() is called.

For BufferedInputStream, implementation is also simple. Every read method can ask the inner buffer to get the next portion of bytes. If the buffer is empty, it may be filled by an appropriate super.read() method. For JScrollPane, implementation is much more complicated. I'll explain why below.

Let's discuss the pros and cons of the inheritance implementation.

  • Pros:

    1. We can implement almost all decorators.
    2. Retaining the type of decorated class is very important. To do that, we can always use a decorated object before decoration. For example, we can use ScrollOverButton instead of JButton in every part of our application, but we cannot use JScrollPane instead of the internal object. Using the inheritance implementation, we can keep the type of the decorated class.
  • Cons:

    1. There are cases when implementation isn't so straightforward. Let's consider that we have an implementation of ScrollOverButton and DebugButton and we need a button that merges the ScrollOver and Debug behaviors together. In this case, the forced choice would be to write the new class ScrollOverDebugButton. If we have the ScrollOverDebugButton implementation, keeping both the ScrollOverButton and DebugButton implementations would be strange. We can add to ScrollOverDebugButton two pairs of methods to turn on/off debug and scroll-over behavior:

         public void setDebug(boolean b);
         public boolean isDebug();
         public void setScrollOver(boolean b);
         public boolean isScrollOver();
      

      Let's consider we have to write a decorator that has U1, U2, ... Un behaviors. We'll write a class named U1U2...UnButton, which has 2n methods:

         public void setU(boolean b);
         public boolean getU;();
      

      In this case, adding the new behavior (Un+1) to the decorator requires adding two methods to the class and changing the decorator implementation. That conflicts with object-oriented design principles and has fatal consequences. Note: The javax.swing.JButton was implemented this way.

    2. Many objects' (visual objects, for example) behaviors are dictated by style preferences. Since style changes are unpredictable, we must adjust our application to these changes. As I point out above, using the inheritance implementation implies changing the decorator's implementation.
    3. Keeping the type of decorated object is not easy. We need to implement every constructor and, sometimes, static methods. Though that isn't difficult, it's still a disadvantage.

The inheritance type of Decorator implementation is not as simple as it first appears. In many cases, we don't know what kind of decorators we'll need for the future; as a result, using this type of implementation will prove difficult for extension and conflicts with object-oriented design principles.

Wrapper

With the wrapper implementation, the main idea is to wrap the decorated object into the Decorator pattern. The Decorator pattern forwards requests to the wrapped object and can perform the new functionality before or after forwarding, or as separate methods.

Now, let's return to our examples and consider them in terms of the wrapper implementation:

  1. BufferedInputStream is a wrapper of InputStream (see java.io.BufferedInputStream in the Java SDK distribution). Regardless of the fact that it extends InputStream, it is a wrapper. In the constructor, BufferedInputStream gets another InputStream, keeps it in the instance variable, and forwards the requests to the underlining InputStream. Note: We can use BufferedInputStream every time we use InputStrem before decoration.
  2. JScrollPane is also a wrapper. It forwards requests to a wrapped Component (called View). Note: We cannot use JScrollPane instead of its underlining component because it doesn't support all of View's functionality. For example, getFont() of JScrollPane returns JScrollPane's font, not View's font.
  3. We can implement DebugButton in this way:

    public class DebugButton extends JButton implements ActionListener
    {
       private JButton butt = null;
       public DebugButton(JButton butt)
       {
          this.butt=butt;
          butt.addActionListener(this);
       }
       // ActionListener
       public void actionPerformed(ActionEvent e)
       {
          System.out.println("debug message for button" + butt);
       }
        . . . . . . . .
       /* About 180 methods look like this:
       any JButton method M(params)
       {
           butt.M(params)
       }
    */
    

    This implementation keeps the type of decorated object (it extends JButton), but it doesn't look so straightforward.

    Note: We cannot use java.lang.reflect.Proxy for redirection because JButton is a class, not an interface.

    Another implementation may be:

    public class DebugButton extends JComponent implements ActionListener
    {
       private JButton butt = null;
       public DebugButton(JButton butt)
       {
          this.butt=butt;
          butt.addActionListener(this);
       }
       public JButton getUnderlineButton()
       {
          return butt;
       }
       // ActionListener
       public void actionPerformed(ActionEvent e)
       {
          System.out.println("debug message for button" + butt);
       }
       
    . . . . . . . .
       /* There we can implement some (not many) selected methods like
          get/setFont, get/setBackground , etc.
       */
    }
    

    This implementation is much more simple, but DebugButton doesn't extend JButton, and we cannot use DebugButton instead of JButton. The JScrollPane implementation is based on this idea.

  4. We'll have the same problems with ScrollOverButton as we saw with DebugButton earlier. Extending the JButton class will result in extra code, but will keep the type of JButton object. Extending JComponent will produce a much simpler and shorter implementation, but will not retain the JButton type.

Let's discuss pros and cons of the wrapper implementation:

  • Pros

    As we see above, we can use this type of implementation for classes with a limited method count (like InputStream). All advantages may be applied to these "short" classes.

    1. Implementation can be simple and we can keep the type of decorated object.
    2. Every decorator implementation can be independent of others.
    3. In many cases, we can use more then one decorator.
  • Con:

    However, for those classes that have many methods, employing the wrapper implementation will result in even longer classes. For visual objects, we need to implement hundreds of methods or sacrifice retaining the type of decorated object.

The wrapper type of Decorator implementation is a real decorator, as described in Design Patterns. It works well for short decorated classes. For long classes, the programmer must choose the lesser of two evils: implement many wrapper methods and preserve the decorated object type, or keep the implementation simple and sacrifice retaining the decorated object type.

External

To illustrate the external implementation, let's take DebugButton and write the DebugDecorator class:

public class DebugDecorator implements ActionListener
{
   public void decorateDebug(JButton butt)
   {
      butt.addActonListenr(this);
   }
   public void undecorateDebug(JButton butt)
   {
      butt.removeActonListenr(this);
   }
   // ActionListener
   public void actionPerformed(ActionEvent evt)
   {
      JButton src = (JButton)evt.getSource();
      System.out.println("debug message for button" + src);
   }
}

In the code above, method decorateDebug() adds ActionListener. Method undecorateDebug removes ActionListener. Method actionPerformed() of ActionListener prints the debug message.

Now, we can write the following code anywhere we need to:

   DebugDecorator decor = new DebugDecorator();
    . . . . . . . . 
   JButton myButt = ...
    . . . . . . . . 
   // Add external decorator 
   decor.decorateDebug(myButt);
   . . . . . . . . .
   // Remove external decorator
   decor.undecorateDebug(myButt);
   . . . . . . . . .

In the same manner, we can write RollOverDecorator. Using both decorators looks like this:

   DebugDecorator debugDecor = new DebugDecorator();
   DebugDecorator rollDecor = new DebugDecorator();
    . . . . . . . . 
   JButton myButt = ...
   . . . . . . . . 
   // Add debug decorator 
   debugDecor.decorateDebug(myButt);
   . . . . . . . . 
   // Add rollOver decorator 
   rollDecor.decorateRollOver(myButt);
   . . . . . . . . .
   . . . . . . . . 
   // Remove debug decorator 
   debugDecor.undecorateDebug(myButt);
   . . . . . . . . 
   // Remove rollOver decorator 
   rollDecor.undecorateRollOver(myButt);

Note: After adding an external decorator we get new behavior without changing one line of code.

We can use the same instance of DebugDecorator for an arbitrary number of JButtons. In other words, one instance of DebugDecorator is enough for the JVM. Of course, we can implement DebugDecorator as a singleton.

I call singletons of this type—that is, a singleton that can have (but does not need) more than one instance—singleton de facto. A regular singleton (singleton de jure) must have only one instance.

Now we can rewrite the DebugDecorator class:

public class DebugDecorator implements ActionListener
{
   private static final DebugDecorator inst = new DebugDecorator();
   public static getInstance()
   {
      return inst;
   }
   public void decorateDebug(JButton butt)
   {
      butt.addActonListenr(this);
   }
   public void undecorateDebug(JButton butt)
   {
      butt.removeActonListenr(this);
   }
   // ActionListener
   public void actionPerformed(ActionEvent evt)
   {
      JButton src = (JButton)evt.getSource();
      System.out.println("debug message for button" + src);
   }
}

And its usage looks like this:

   JButton myButt = ...
    . . . . . . . . 
   // Add external decorator 
   DebugDecorator.getInstance().decorateDebug(myButt);
   . . . . . . . . .
   // Remove external decorator
   DebugDecorator.getInstance().undecorateDebug(myButt);
   . . . . . . . . .

Now we can add to DebugDecorator the decorate() and undecoratedDebugContainer() methods:

public void decorateDebugContainer(JComponent container)
{
   Component[] comps = container.getComponents();
   for(int i = 0 ; i<comps.length; i++)
   {
      if(JButton.class.isInstance(comps[i]))
      {
         comps[i].addActionListener(this);
      }
   }
}
public void undecorateDebugContainer(JComponent container)
{
   Component[] comps = container.getComponents();
   for(int i = 0 ; i<comps.length; i++)
   {
      if(JButton.class.isInstance(comps[i]))
      {
         comps[i].removeActionListener(this);
      }
   }
}

From now on, we can use DebugDecorator for containers also (e.g., ToolBar), which is convenient.

We can write the same implementation for RollOverDecorator, but not for BufferedInputStream because we don't have the appropriate Listener or callback.

Let's discuss pros and cons of the external implementation.

1 2 Page 1
Page 1 of 2