Design Patterns - The Decorator Pattern
Tarbucks needs to add a new beverage to the system. Shouldn’t be a problem! Right? But, it would seem their current class diagram is somewhat overwhelming.
Before blaming the previous developer, let’s try and break this down. We appear to have an abstract superclass- Beverage
. Every Beverage
sub-type takes care of their cost()
implementation. Let’s look at HouseBlend
to simplify things.
It seems that a new sub-type gets created when additional condiments are added. Not ideal. We could add our new sub-type, but we’d only be contributing to a duplication problem that plagues this design.
We need to refactor. Let’s start by moving the condiments to the superclass.
Let’s bring all the subclasses back and see how things look.
Not bad. We are down to five classes.
Although, if we need to add a new condiment we’d have to modify the Beverage
superclass, which is undesirable. Our design should adhere to the open-closed principle which states; classes should be open for extension but closed for modification. Adding a new condiment each time would certainly violate this principle.
Inheritance hasn’t been serving us well in this scenario; we need an alternative approach. We should start with a Beverage
and decorate it with condiments. Let’s say we need a Dark Roast with Mocha and Whip, here is how we would create it:
- Make a
DarkRoast
object - Decorate it with a
Mocha
object - Decorate it with a
Whip
object - Call
cost()
and rely on delegation to add on the condiment costs
Decorate? Think of decorators as “wrappers”. Maybe a visual representation would better serve this concept.
1. Make a DarkRoast
object
2. Decorate it with Mocha
object
3. Decorate it with Whip
object
4. Call cost()
and rely on delegation to add on the condiment costs
Let’s try a solution using the decorator pattern.
Okay, enough diagrams. Let’s do some code. To begin let’s look at the Beverage
class which doesn’t need to be altered from it’s original design. The getDescription()
method is defined for us while cost()
needs to be implemented by subclasses.
public abstract class Beverage {
String description = "Unknown beverage";
public String getDescription() {
return description;
}
public abstract double cost();
}
Now we create the CondimentDecorator
. This abstract class needs to be interchangeble with Beverage
, so we extend the Beverage
class. Why the abstract getDescription()
method? We need this to output condiment names.
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}
Now we’re done with the base classes, let’s look at the HouseBlend
concrete implementation of Beverage
. Remember we need to specify description
and implement the cost()
method.
public class HouseBlend extends Beverage {
public HouseBlend() {
description = "House Blend";
}
public double cost() {
return 0.99;
}
}
We’ve got our abstract component (Beverage
), concrete components (HouseBlend
) and our abstract decorator (CondimentDecorator
). Now we need to implement our concrete decorators. Let’s start with Mocha
. We need to extend the CondimentDecorator
and store a reference to the Beverage
to be decorated. Notice getDescription()
and cost()
, they both delegate the call to this.beverage
before adding their description/cost.
public class Mocha extends CondimentDecorator {
// The berverage to which the condiment will be applied
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return this.beverage.getDescription() + ", Mocha";
}
public double cost() {
// Return the cost plus the condiment cost
return this.beverage.cost() + 0.10;
}
}
Finally we can make a coffee.
Beverage houseBlend = new HouseBlend();
houseBlend = new Whip(houseBlend);
houseBlend = new Mocha(houseBlend);
Beverage darkRoast = new DarkRoast();
darkRoast = new Soy(darkRoast);
darkRoast = new Vanilla(darkRoast);
System.out.println(houseBlend.getDescription() + houseBlend.cost())
System.out.println(darkRoast.getDescription() + darkRoast.cost())
// Outputs:
// HouseBlend, Mocha, Whip 1.69
// DarkRoast, Soy, Vanilla 1.97
Final thoughts
- Inheritance is not the best way to extend functionality. Composition offers greater flexibility
- It’s possible through a coding error to wrap the concrete component incorrectly and not call the outmost wrapper
- Due to the above point, the decorator pattern should be used in combination with the factory pattern
- The decorator can result in a lot of subclasses which can get confussing for new developers
- Care should be taken that client code doesn’t rely on a single concrete component always use abstractions. In our example client code should use the abstract class
Beverage