Decorator - Programming in the Large with Design Patterns (2012)

Programming in the Large with Design Patterns (2012)

Chapter 5. Decorator

Introduction

The Decorator design pattern uses object composition and delegation to extend the behavior of existing classes in a way that is both lightweight and flexible. To understand the value of this approach, consider the other options for extending the behavior of a class.

The most straightforward way of extending the behavior of a class is to simply modify the class to include the new behavior. For example, the class in Figure 43 offers features A, B and C. Adding a new feature is a simple matter of adding the code for the new feature. The biggest problem with this approach is that it violates the open-closed principle. The open-closed principle states that classes should be open for extension, but closed for modification. Because adding a new feature requires changing the exiting class, the class clearly is not closed for modification. Designs that follow the open closed principle are preferred because there is less risk of collateral damage when adding new features.

Figure 43 Adding new features requires changing existing code

Another option for extending the behavior of an existing class is inheritance. With inheritance, class behavior is extended by adding a subclass and overriding one or more methods in the base class.

Figure 44 Adding new features via subclassing

Adding new features via subclassing complies with the open-closed principle because behavior is extended without making changes to existing code. However, because the protected interface of the base class is exposed to subclasses, inheritance tends to weaken the encapsulation barrier around the base class—a condition known as the fragile superclass problem. When extending behavior via inheritance, existing classes are more vulnerable to change than with other methods such as delegation where dependencies are limited only to the public interface of classes.

Another disadvantage of using inheritance to extend the behavior of a class is that it creates a compile-time structural relationship between the features being added and the classes being extended. Feature combinations must be anticipated at design time. That is more rigid than adding and removing features at runtime via object composition.

One final situation where inheritance performs poorly is when different combinations of features are valid. The main problem is the potential for a combinatorial explosion of subclasses. For example, Figure 45 shows 7 different subclasses--one for each combination of features A, B and C.

Figure 45 Inheritance doesn’t work well when different combinations of features are valid

The examples above highlight some of the weaknesses associated with common approaches to extending the behavior of a class. The Decorator design pattern provides an alternate approach that addresses many of these weaknesses.

To better understand the concept behind the Decorator design pattern, consider the following fictional account of how an online retailor used the Decorator design pattern to simplify product development.

Once upon a time there was an online retailer planning to offer e-book readers in 3 basic form factors:

1. Standard E-book

2. Large E-book

3. Tablet

This retailer also planned to offer various feature combinations with each base model:

1. Wi-Fi

2. 3G Wireless

3. Discrete text advertisements for $30 off

With 3 base models and 3 optional features, there were 24 (3 * 2^3) different configurations that needed to be manufactured: Standard, Standard + Wi-Fi, Standard + Wi-Fi + 3G, Standard + 3G, etc. Some might be less popular than others, but since the retailer was going after the long tail, it was important to offer all configurations.

Trying to solve the problem using inheritance would result in a very unwieldy inheritance hierarchy:

Figure 46 Trying to accommodate different feature combinations results in an unwieldy inheritance hierarchy

Not looking forward to the prospect of designing and maintaining 24 different individual device configurations, our fictional retailer challenged its engineers to come up with a breakthrough design that would not only make it easy to configure the current 24 different retail options but also reduce the effort needed to add new features and base models in the future.

The engineers did not disappoint. They proposed a brilliant design with 3 separate base units (standard, large and tablet) and "skins" for each feature. A skin is an invisible slipcover that adds a particular feature. When a customer orders say a Standard E-book with Wi-Fi and discrete text advertisements, a warehouse worker would take a Standard base E-book from a bin and add two transparent slipcovers, one for Wi-Fi and another for discrete text advertisements. Because the slipcovers are transparent and offer the same basic interface as the base unit, customers are unaware that certain features are being added with slipcovers.

Congratulations! You now understand the basic concept behind the decorator design pattern. The decorator design pattern provides a way of attaching additional responsibilities to an object dynamically. Objects to be extended are wrapped with decorator objects. A decorator object implements the same interface as the object it decorates. New features are added before, after or in place of delegating requests to the wrapped object. Figure 47 shows a conceptual diagram for a standard E-book reader decorated with discreet advertisements and Wi-Fi.

Figure 47 Conceptual diagram for Decorator design pattern

Intent

The Decorator design pattern provides an alternative to class inheritance for extending the behavior of existing classes. It uses object composition rather than class inheritance for a lightweight flexible approach to adding responsibilities to objects at runtime. It is especially useful when different combinations and permutations of features are permitted.

Solution

Figure 48 shows the main components and general structure of the Decorator design pattern.

Figure 48 General structure of the Decorator design pattern

The two main groups of classes in the Decorator design pattern are concrete components and concrete decorators. Concrete components contain the base functionality that is extended or decorated with features defined in concrete decorators.

To add a feature to a component, you create an instance of the component and pass this instance to the constructor for the decorator that defines the feature you want to add to the component.

ConcreteComponent cc = new ConcretComponent();

ConcreteDecorator decoratedComponent = new ConcreteDecorator(cc);

Concrete decorators inherit from the abstract class Decorator. Decorator is a convenience class. It keeps a reference to the component being decorated and implements the same interface as the component. This allows decorator objects to be used transparently anywhere component objects are expected.

The default behavior for a decorator is to forward requests to the decorated component. Concrete decorators inherit the default decorator behavior and optionally add their own behavior before, after or in place of this default behavior.

Decorators wrap components, which may be concrete components or other decorators. This makes it possible to have an arbitrarily long chain of decorators leading to a concrete component at the end.

The UML sequence diagram in Figure 49 shows the interactions of a concrete component and two decorators. ConcreteDecorator1 adds feature A and ConcreteDecorator2 adds feature B. Not shown in the sequence diagram is the setup logic that configured the concrete component with two decorators:

ConcreteComponent cc = new ConcretComponent();

ConcreteDecorator1 cd1 = new ConcreteDecorator2(cc);

ConcreteDecorator2 cd2 = new ConcreteDecorator1(cd1);

cd2.operation();

Figure 49 Behavior diagram for the Decorator design pattern

Sample Code

The introduction made the case for using the Decorator design pattern rather than inheritance to achieve a more flexible design for an imaginary line of E-book readers. Figure 50 shows the class diagram for the design. Notice it has three concrete components (StandardEbook, LargeEbook and Tablet) and three concrete decorators (Wi-Fi, 3GWireless and DiscreetAdvertisements).

Figure 50 Class diagram for line of E-book readers using Decorator design pattern

The following program implements the design and demonstrates its use by decorating a Standard E-book with Wi-Fi and discreet text advertisements. The decorated component searches for Wi-Fi networks when powered on and displays a discreet text advertisement when transitioning from one page to the next. Figure 47 in the introduction shows a conceptual diagram for the implementation below.

public class DecoratorExample {

public static void main(String[] args) {

StandardEbook baseModel = new StandardEbook();

E_Book decoratedEbook = new Wi_Fi(

new DiscreetAdvertisements (baseModel));

decoratedEbook.powerOn();

decoratedEbook.nextPage();

decoratedEbook.powerOff();

}

}

interface E_Book {

void powerOn();

void powerOff();

void nextPage();

}

abstract class Decorator implements E_Book {

private E_Book component;

public Decorator (E_Book ebook) {

component = ebook;

}

public void powerOn() {

component.powerOn();

}

public void powerOff() {

component.powerOff();

}

public void nextPage() {

component.nextPage();

}

}

// Concrete Component

class StandardEbook implements E_Book {

public void powerOn() {

System.out.println("Power on Standard E-Book");

}

public void powerOff() {

System.out.println("Power off Standard E-Book");

}

public void nextPage() {

System.out.println("Next page Standard E-Book");

}

}

// Concrete Decorator

class Wi_Fi extends Decorator {

public Wi_Fi(E_Book component) {

super (component);

}

public void powerOn() {

super.powerOn();

System.out.println("Scan for Wi-Fi networks");

}

}

// Concrete Decorator

class DiscreetAdvertisements extends Decorator {

public DiscreetAdvertisements(E_Book component) {

super (component);

}

public void nextPage() {

super.nextPage();

System.out.println("Advertisement: Buy more stuff today!");

}

}

Discussion

One of the benefits of knowing certain design patterns is that it makes it easier to learn class libraries based on these patterns. The Decorator design pattern illustrates this well. Developers not familiar with the Decorator design pattern are often overwhelmed by the vast number of classes that make up the Java I/O class library. Those familiar with the Decorator design pattern have a much easier time of learning how to use Java I/O classes because the library is based on the Decorator design pattern.

Figure 51 shows some of the key byte stream input data classes in the java.io package. Seeing the classes organized according to the structure of the Decorator design pattern makes it easier to understand the function of each one.

Figure 51 Classes in the java.io package follow the Decorator design pattern

The concrete components in Figure 51 represent input sources of raw data. The concrete decorators represent features that can be added to input streams. The concrete components can be decorated with different combinations and permutations of features offered by the concrete decorators. For example, the code fragment below shows a file input stream wrapped with features for buffering and pushing back or "unreading" data.

FileInputStream f = new FileInputStream("input.txt");

BufferedInputStream b = new BufferedInputStream(f);

PushbackInputStream p = new PushbackInputStream(b);

int c = p.read();

while (c != ' ') {

// Process c

. . .

c = p.read();

}

// Push back non-blank character read.

p.unread(c);

This example also brings to light one of the downsides of using the Decorator design pattern: users must contend with a large number of small classes even for routine tasks. In the example above, three classes had to be constructed and linked just to read bytes from a file.

Related Patterns

Both Decorator and Adapter delegate requests to a wrapped object. The principle difference between the two patterns is Decorator adds new behavior without changing the interface whereas Adapter keeps the same basic behavior but changes the interface.

Figure 52 Principle difference between Decorator and Adapter

The Factory method design pattern is sometimes used with the Decorator design pattern in order to encapsulate the setup procedure needed to construct and link decorated components. For example, here is a Factory method for constructing a decorated E-book:

public E_Book getFeaturedModel() {

return new Wi_Fi(

new DiscreetAdvertisements (

new StandardEbook()));

}