Decorator Design Pattern

Think of our universally favorite ice creams!
We can have our individual flavors like vanilla, chocolate, strawberry and so many others.
We can even mix them too as per our choice.
A decorator can help us of build our favorite flavors, including the mixed flavors, with ease !

It’s a structural design pattern that allows us to create multiple flavors of our product, which we can even mix and match to create newer flavors.

One way to inherit the existing features is through sub-classing which is also know as the inheritance approach. The other one being the composition. The decorator follows the later one which makes it lot more flexible because of the following reasons:

  • First, in case of a sub-class, it has a fixed parent to inherit the features. But, using composition, we can inherit from a family of products as long as they follow the required interface. In short, the sub-classing provides fixed compile time inheritance, whereas the composition provides flexible dynamic inheritance.
  • Second, it also allows us to mix multiple flavors of the product as layers within each other just like our ice creams. The different layers here act as a chain of processors, each layer building it’s features on top of the underlying layer.

So, having the concepts in mind, let us see how it really works with some examples.

 

Why to Use and How does it work?

In this section we will be discussing on two example. In the first, we will see the significance of using a decorator pattern. And, in the second, we will look at how the pattern works.

Example-1 – Java IO Package

When we are from java background, the best place to learn decorators is to look at the IO package. The below diagram shows a set of IO classes under the InputStream.

Clearly, there can be a variety of sources. Even the data formats may vary widely. Again, we may want to read into characters, byte streams or objects. So, with so many factors, its not possible put the processing logic into a simple hierarchy even if many features are dependent on each other. Hence, the best answer here is the composition to create different combination using smaller parts.

Here are some examples in this regard :

/* Composition makes the IO unit highly flexible */
		  
//1. Read the data from a file as byte stream
FileInputStream fis = new FileInputStream("t.tmp");
		  
//2. Read the data from a byte array as a byte stream
ByteArrayInputStream bais = new ByteArrayInputStream("something something ..".getBytes());
	      
//3. Read Objects from a file = Read the byte stream + deserialize the byte stream into objects 
ObjectInputStream ois = new ObjectInputStream(fis);

//3.1 Make the reading faster using a BufferedInputStream. 
// Combination : ObjectInputStream + BufferedInputStream + FileInputStream
BufferedInputStream bis = new BufferedInputStream(fis); 
ObjectInputStream oisFast = new ObjectInputStream(bis);  
	
//4. Read objects from .gz file = Read the data from the file as  byte stream 
// + deflate the .gz format + deserialize the resultant byte stream into objects 
// Combination : ObjectInputStream + GZIPInputStream + FileInputStream
GZIPInputStream gzis = new GZIPInputStream(fis);
ObjectInputStream objis = new ObjectInputStream(gzis);

In the above examples, we call each of the classes which enclose an existing feature to build a newer feature as the decorators.

  • Due to a common interface we are able to replace the classes with each other during composition.
  • Each new layer builds on the features provided by the underlying layer.
  • Because of the flexibility to mix and match, we are able to dynamically convert the same input stream to address multiple needs.
Example-2 – Fancy Cars

Lets say we have a base version of our fancy car that runs only on the roads. But, we are planning it to sail, fly, climb in the future.

In this example we will see how can we easily build our advanced cars by using decorators. The diagram below shows the design.

Each decorator refers to a concrete FancyCar, so that it can utilize existing features from this reference.

For example, as shown below, the getDescription() in BoatingCarDecorator is mixing its logic with the output from its enclosed FancyCar.

Similarly, in case of the super car, the BoatingCarDecorator adds the sailing feature to a FancyCar that can run only on the roads. Again, the top layer of the FlyingCarDecorator adds the flying feature to a FancyCar that can both sail and run on the roads.

Contrary to getDescription() method, the drive() method usages the existing features conditionally instead of building on it. It’s just showing the kind of flexibility we have in using the multiple layers in a decorator.

Source Code

Below is the source code for our decorator demo on a fancy car.

1. The RoadRacer is our base car version that runs only on the roads. But, we are planning for it to sail, fly, climb in the future.

package spectutz.dp.struct.decorator.car.vo;

public class RoadRacer implements IFancyCar{
	
	@Override
	public String getDescription() {
		return "RoadRacer";
	}

	@Override
	public void drive(RouteType routeType) {
		if(RouteType.ROAD == routeType) {
			System.out.println("Racing on the road!");		
		}else {
			System.out.println("Does not support route type "+ routeType);
		}
	}
}
package spectutz.dp.struct.decorator.car.vo;

public interface IFancyCar {
	public String getDescription();
	public void drive(RouteType routeType);
}
package spectutz.dp.struct.decorator.car.vo;

public enum RouteType {	
	ROAD, WATER, AIR;
}

2. While adding the new capabilities, we do not want to disturb the original product. Hence, we are building these decorators to repackage the existing car with additional features.

package spectutz.dp.struct.decorator.car.decorators;
import spectutz.dp.struct.decorator.car.vo.IFancyCar;
import spectutz.dp.struct.decorator.car.vo.RouteType;

public class BoatingCarDecorator extends AbstractCarDecorator{
	
	public BoatingCarDecorator(IFancyCar fancyCar) {
		super(fancyCar);		
	}

	@Override
	public String getDescription() {
		//The decorator, modifying the feature used in the underlying fancyCar
		return fancyCar.getDescription() +" plus BoatingCar";
	}
	
	@Override
	public void drive(RouteType routeType) {
		if(RouteType.WATER == routeType) {
			//Feature from the decorator
			System.out.println("Sailing fast with pointed nose !");		
		}else {
			//Feature from the underlying fancyCar
			fancyCar.drive(routeType);
		}
	}
}
package spectutz.dp.struct.decorator.car.decorators;
import spectutz.dp.struct.decorator.car.vo.IFancyCar;
import spectutz.dp.struct.decorator.car.vo.RouteType;

public class FlyingCarDecorator extends AbstractCarDecorator{
	
	public FlyingCarDecorator(IFancyCar fancyCar) {
		super(fancyCar);
	}
	public String getDescription() {
		//The decorator modifying the feature used in the underlying fancyCar
		return fancyCar.getDescription() +" plus FlyingCar";
	}

	@Override
	public void drive(RouteType routeType) {
		if(RouteType.AIR == routeType) {
			//Feature from the decorator
			System.out.println("Flying high with wings spread ! ");		
		}else {
			//Feature from the underlying car
			fancyCar.drive(routeType);
		}
	}
}
package spectutz.dp.struct.decorator.car.decorators;
import spectutz.dp.struct.decorator.car.vo.IFancyCar;
import spectutz.dp.struct.decorator.car.vo.RouteType;

//The base decorator
public abstract class AbstractCarDecorator implements IFancyCar{
	
	protected IFancyCar fancyCar;
	
	public AbstractCarDecorator(IFancyCar fancyCar) {
		this.fancyCar = fancyCar;	
	}
	public abstract String getDescription();
	public abstract void drive(RouteType routeType); 
}

3. Finally, the demo code shows how the repackaging with the decorators, creates the newer versions of the underlying product.

package spectutz.dp.struct.decorator.car;
import spectutz.dp.struct.decorator.car.decorators.BoatingCarDecorator;
import spectutz.dp.struct.decorator.car.decorators.FlyingCarDecorator;
import spectutz.dp.struct.decorator.car.vo.IFancyCar;
import spectutz.dp.struct.decorator.car.vo.RoadRacer;
import spectutz.dp.struct.decorator.car.vo.RouteType; 

public class FancyCarDecoratorDemo {
	
	public static void main(String[] args) {
		
		RoadRacer roadRacer = new RoadRacer();
		System.out.println("\nMy Fancy Car: "+roadRacer.getDescription());
		roadRacer.drive(RouteType.ROAD);
		roadRacer.drive(RouteType.WATER);
		roadRacer.drive(RouteType.AIR);
		
		BoatingCarDecorator roadPlusWaterCar = new BoatingCarDecorator(roadRacer);
		System.out.println("\nMy Fancy Car: "+roadPlusWaterCar.getDescription());
		roadPlusWaterCar.drive(RouteType.ROAD);
		roadPlusWaterCar.drive(RouteType.WATER);
		roadPlusWaterCar.drive(RouteType.AIR);

		FlyingCarDecorator superCar = new FlyingCarDecorator(roadPlusWaterCar);
		System.out.println("\nMy Fancy Car: "+superCar.getDescription());		
		superCar.drive(RouteType.ROAD);
		superCar.drive(RouteType.WATER);
		superCar.drive(RouteType.AIR);
	}
	
}
My Fancy Car: RoadRacer
Racing on the road 
Does not support route type WATER
Does not support route type AIR

My Fancy Car: RoadRacer plus BoatingCar
Racing on the road 
Sailing fast with pointed nose !
Does not support route type AIR

My Fancy Car: RoadRacer plus BoatingCar plus FlyingCar
Racing on the road 
Sailing fast with pointed nose !
Flying high with wings spread ! 

 

Conclusion

The decorators provide a lot of flexibility in creating newer flavors from the existing products. Because of the ability to mix and match, decorators can provide a significant advantage over sub-classing for a given solutions.

However, importantly, the flexibility to mix and match can be error prone and difficult to identify at compile time. Hence, we should be careful in using the right set decorators and also in the right order.