Understanding JavaScript Decorator Patterns

The decorator pattern works by extending an object’s behavior without changing the original object itself.

What exactly does this mean?

Imagine that you’ve just bought yourself a new phone. However, carrying the phone around in its current form feels risky. What if you drop it? What if it gets scratched? What if you only want to carry your phone with you and nothing else?

The solution to this is to get one of those phone cases that also doubles up as a wallet with card slots. Congratulations, you’ve just technically decorated your phone by extending its features. The phone case is removable and interchangeable with other compatible cases. It adds extra features to the phone but doesn’t change the core functionality of the phone itself.

That’s what a decorator pattern does. It adds features and functionality while the original object remains separated from it. The dependency is a one way relationship, with the add-on extension potentially relying on data provided by the original object. However, the original object doesn’t need the extension to work properly.

So what does this look like in code?

Let’s revisit our cart object and write it again.

//the base object
 class Cart{
   constructor(name, sku, price){
     this.name = name;
     this.sku = sku;
     this.price = price;
   }
   
   getCart(){
     return 'in your cart is a ' + this.name;
   }
 }
 
 const cart = new Cart("clock", 2984, 15.99);
 console.log(cart.getCart());
 
 //extending with the decorator
 //the entry decorator 
 class CartDecorator{
   constructor(cart){
     this.cart = cart;
   }
   getCartItem(){
     return this.cart.getCart();
   }
 }
 
 //the extension
 class ShippingDecorator extends CartDecorator{
   getShipping() {
     return 'shipping for '
            + this.cart.name
            + ' is $' 
            + this.cart.price*0.25; 
   }
 }
 
 const shipping = new ShippingDecorator(cart);
 console.log(shipping.getShipping());

In the above example, extending the decorator has two classes. Why? The first class can be viewed as the entry class that connects the base object with its extension whilst keeping the original object separated. In order for the extension to ‘stick’, it needs something to act as the intermediary.

In a way, this looks sort of like the composite pattern through the usage of extends - except it's not quite exactly the same. The composite pattern is often used to compose relationships between a series of standalone objects. A decorator pattern is a dependency pattern that creates extensions against the current existing objects. This is useful for situations where a particular object role may change.

For example, you might have a user and this user object can have multiple roles that determine access to certain things. You start off using a composite pattern to form the relationship between user and roles, but the type of roles and its associated code can be extended using the decorator pattern.

Or alternatively, you have a cart with a shipping class composite. You can decorate your shipping class and extend it with different shipping categories, with each category having its own set of methods and properties.

The point of a decorator pattern is to extend your object’s features without changing the object itself. The decorator has access to the object it’s extending, but this relationship is one way. This is to ensure that the relationship is loosely coupled and the original object doesn’t depend on the extensions to exist and function.