Decorator is one of the Structural Design Patterns.
You’re about to start with gift wrapping for a birthday party. A delicate, fragile gift has to be bubble-wrapped. It is followed by placing it safely in a cardboard box. The box itself may be wrapped with shining wrapping paper. Finally, finishing it off with an elegant winding of a satin ribbon around it.
These layers and more are added to or removed from the gift as randomly as our creative thoughts flow. The gift object, however, remains unperturbed. But, the packaging makes it look a lot better for the handover.
Similar to the example quoted above, the decorator pattern is merely a conceptualized way of enhancing the properties or functionalities of objects with respect to their design.
The decorator aids in adding or removing properties or functionalities to objects without having to alter the structure of the object. To emphasize, the original object remains unmodified/constant. The new features are essentially wrapped around the object without coming in contact with it.
Here is another example of having to build models of Headphones. There are different kinds of them. How about considering Wireless and Waterproof Headphones for now? Let’s take a look at a probable initial design for this:
We have a concrete Headphone
class. WirelessHeadPhone
and WaterproofHeadPhone
are its two subclasses.
class Headphone {
constructor(model, color) {
this.model = model;
this.color = color;
}
getPrice() {
return 100;
}
}
class WirelessHeadPhone extends Headphone {
constructor(model, color) {
super(model, color);
this.isWired = false;
}
getPrice() {
return 150;
}
}
class WaterproofHeadPhone extends Headphone {
constructor(model, color) {
super(model, color);
this.isWaterproof = true;
}
getPrice() {
return 120;
}
}
What if now, there comes a new requirement to make the headphones both waterproof and wireless in combination? What would you do? Should my new WaterProof and Wireless Headphone be extending the WirelessHeadPhone class? Inheritance doesn’t provide a way to subclass from multiple classes. A subclass can only have one parent class. How do I decide which class to extend it from now? Extending from any class would not make too much of a difference here. I’d give up and go for doing something like this:
class WaterProofAndWirelessHeadphone extends Headphone {
constructor(model, color) {
super(model, color);
this.isWaterproof = true;
this.isWired = false;
}
getPrice() {
return 170;
}
}
This definitely solves the problem. Just when you start thinking you are done with this, now the company wants to introduce Headphones for Kids.
Now you have another class, the Headphone class needs to be extended to.
Finally, this is what we arrive at:
class BabyEarHeadphone extends Headphone {
constructor() {
super(model, color);
this.size = 'Small';
}
getPrice(model, color) {
return 80;
}
}
The requirements just don’t stop there. You may have to have a number of permutations on each of the existing features and will have new incoming features.
This shows that adding a subclass for every new requirement makes them too many in number. This results in what we call class explosion.
Here is where Decorator comes into play, providing a much more elegant and flexible alternative solution.
We have now seen that adding new features to a class can be achieved using class extension/inheritance. But for scenarios where the depth of inheritance increases, it gets out of hand, resulting in too many subclasses. Maintenance of design as such would turn into a nightmare. Decorator pattern helps avoid this problem.
These new features are attached to the objects, using Decorator Pattern, only during runtime, not before that.
The decorator’s abstraction has two flavors to it:
- The decorator itself acts as an interface to the object it wraps.
- The decorator has the properties of the object it wraps.
To keep everything as simple as possible, consider an example for cupcake making. CupCake
here is a concrete class. Adding sprinkles, chocolate chips, frosting are its decorators. The pricing for a cupcake depends on the decorators added to it. In its simplest form, the decorator pattern looks like this:
class CupCake {
constructor(flavour, color) {
this.flavour = flavour;
this.color = color;
this.cost = 3;
}
}
A cupcake
is an object that needs to be decorated.
Let’s look at our first decorator, addSprinkles
. The decorator accepts an instance of Cupcake
as its input. The decorator now wraps the original object to append an additional property to it, keeping the object’s structure intact and not modifying it.
//decorator 1
const addSprinkles = function(cupcake) {
const cost = cupcake.cost + 1;
return {...cupcake, hasSprinkles: true, cost};
}
We can allow an unlimited number of decorators to wrap around the object, just by sending the instance of it around to each decorator responsible for their individual capability to enhance the object’s functionalities.
//decorator 2
const addSkittles = function(cupcake) {
const cost = cupcake.cost + 2;
return {...cupcake, hasSprinkles: true, cost};
}
Finally, this is the Cupcake decorated with sprinkles and/or with skittles!
const vanilla = new CupCake('vanilla', 'blue');
const sprinkledVanilla = addSprinkles(vanilla);
const skittleVanilla = addSkittles(vanilla);
const fullDecoratedVanilla = addSkittles(sprinkledVanilla); //A combination of both sprinke decorator and skittle decorator.
console.log(vanilla.cost); //3
console.log(sprinkledVanilla.cost); //4
console.log(skittleVanilla.cost); //5
console.log(fullDecoratedVanilla.cost); //5
Note that Javascript is a dynamic language. The ability to extend its functionality is extremely simple, an inherent feature of the language in itself. For a statically typed programming language, however, the decorator pattern’s flexibility makes much of a difference. The advantage is with the capability to adapt to changes during the run time, especially when compared to the compile-time changes that inheritance provides.
Get my free e-book to prepare for the technical interview or start to Learn Full-Stack JavaScript