Part 06: Decorator Design Pattern

Part 06: Decorator Design Pattern

What Is A Decorator Design Pattern

Decorator is a structural design pattern, structural design patterns gather objects and classes together in a larger structure while keeping it well organized and flexible.

Decorator design pattern allows you to modify the object dynamically at runtime by adding new functionalities without affecting existing functionalities. It is done by wrapping the original object with the wrapper objects(decorators) having the required functionalities.

Explanation Using An Example

Let's take an example, we have to build a notification feature to notify people about anything the application asks for. The app should have different notification types like E-mail notifications, push notifications for desktop, mobile, web and SMS notification.

Problem

We might first think to make a base class notifier and make EmailNotifier, WebPushNotifier, MobilePushNotifier, DesktopPushNotifier and SMSNotifier it's sub-classes, seems simple. But client app might need more than one notifiers, this could be done by inheritence but there are some issues, many languages do not support multiple inheritence and our base class will be stuffed with sub-classes, code will be repeated a lot and it will be a mess.

Solution

We can solve these issues using association. In which an object has a reference to another object and delegates it some work. Let's see how to use this approach in implementing decorator design pattern.

A wrapper is an object that is linked to the main object, it has same set of methods as the main object. so from client's perspective both are identical. So we have a basic notifier and decorators which will enable us to have more kinds of notifications wrapped around basic notifier. Now we won't have to create tons of sub-classes, just a decorator with same sets of methods.

The client code will now just need to wrap the decorator it needs around the basic notifier object, the resulting object would be structured as a stack, wrapped one on another. The client will be working with the last decorator, it does not have to care about which object is it working with.

Structure

decorator design pattern.jpg

Implementation

Note: I won't be covering implementation of sending notification instead I will just use console logs to show notification messages to keep the code shorter and focused on decorator design pattern.

We have made a basic Notifier class having a send method and 5 decorator classes EmailDecorator, SMSDecorator, WebPushDecorator, MobilePushDecorator and DesktopPushDecorator , implementing send method and having a wrappe referencing the object that would be wrapped by that decorator. The send method in decorator will peroform it's task and then call wrappe's send method.

// Notifier class defination

function Notifier() { }
//send message method for basic notifier
Notifier.prototype.send = function(message) {
  console.log(`Basic Notifier : ${message}`);
}

// Email notification decorator
function EmailDecorator(wrappe) {
  // wrappe will be the obbject that will be decorated by this decorator
  this.wrappe = wrappe;
}

// send method becasue the decorators must have similar functionalities so it seems identical to the client
EmailDecorator.prototype.send = function(message) {
  console.log("email : " + message);
  // call the wrappe's send method
  this.wrappe.send(message);
}


function SMSDecorator(wrappe) {
  this.wrappe = wrappe;
}

SMSDecorator.prototype.send = function(message) {
  console.log("sms : " + message);
 // call the wrappe's send method
  this.wrappe.send(message);
}

function WebPushDecorator(wrappe) {
  this.wrappe = wrappe;
}

WebPushDecorator.prototype.send = function(message) {
  console.log("web push notification : " + message);
 // call the wrappe's send method
  this.wrappe.send(message);
}

function MobilePushDecorator(wrappe) {
  this.wrappe = wrappe;
}

MobilePushDecorator.prototype.send = function(message) {
  console.log("mobile push notification : " + message);
 // call the wrappe's send method
  this.wrappe.send(message);
}

function DesktopPushDecorator(wrappe) {
  this.wrappe = wrappe;
}

DesktopPushDecorator.prototype.send = function(message) {
  console.log("desktop push notification : " + message);
   // call the wrappe's send method
  this.wrappe.send(message);
}

function run() {

  // config for notifications

  let config = {
    smsEnabled: true,
    emailEnabled: true,
    webPushEnabled: true,
    mobilePushEnabled: true,
    desktopPushEnabled: true,
  }

  // basic notifier objects
  let notifications = new Notifier();

  if (config.smsEnabled)
    //wrapping basic notifier in decorator
    notifications = new EmailDecorator(notifications);

  if (config.emailEnabled)
    notifications = new SMSDecorator(notifications);

  if (config.webPushEnabled)
    notifications = new WebPushDecorator(notifications);

  if (config.mobilePushEnabled)
    notifications = new MobilePushDecorator(notifications);

  if (config.desktopPushEnabled)
    notifications = new DesktopPushDecorator(notifications);

  notifications.send("This is trial notification");
}

run();

A friend of mine suggested that I should use classes too so here is the implementation using classes, both outputs will be the same.

// Notifier class defination

class Notifier{

  //send message method for basic notifier
  send(message){
    console.log(`Basic Notifier : ${message}`);
  }
}

// Email notification decorator
class EmailDecorator {

  // wrappe will be the obbject that will be decorated by this decorator
  constructor(wrappe){
    this.wrappe = wrappe;
  }

  // send method becasue the decorators must have similar functionalities so it seems identical to the client
  send(message){
    console.log("email : " + message);
    // call the wrappe's send method
    this.wrappe.send(message);
  }
}

// SMS notification decorator
class SMSDecorator {

  constructor(wrappe){
    this.wrappe = wrappe;
  }

  send(message){
    console.log("sms : " + message);
    // call the wrappe's send method
    this.wrappe.send(message);
  }
}

// Web notifications decorator
class WebPushDecorator {

  constructor(wrappe){
    this.wrappe = wrappe;
  }

  send(message){
    console.log("web push notification : " + message);
    // call the wrappe's send method
    this.wrappe.send(message);
  }
}

// Mobile notifications decorator
class MobilePushDecorator{

  constructor(wrappe){
    this.wrappe = wrappe;
  }

  send(message){
    console.log("mobile push notification : " + message);
    // call the wrappe's send method
    this.wrappe.send(message);
  }
}

// Desktop notifications decorator
class DesktopPushDecorator{

  constructor(wrappe){
    this.wrappe = wrappe;
  }

  send(message){
    console.log("desktop push notification : " + message);
   // call the wrappe's send method
    this.wrappe.send(message);
  }
}

function run(){

  // config for notifications

  let config = {
    smsEnabled : true,
    emailEnabled : true,
    webPushEnabled : true,
    mobilePushEnabled : true,
    desktopPushEnabled : true,
  }

  // basic notifier objects
  let notifications = new Notifier();

  if(config.smsEnabled)
    //wrapping basic notifier in decorator
    notifications = new EmailDecorator(notifications);

  if(config.emailEnabled)
    notifications = new SMSDecorator(notifications);

  if(config.webPushEnabled)
    notifications = new WebPushDecorator(notifications);

  if(config.mobilePushEnabled)
    notifications = new MobilePushDecorator(notifications);

  if(config.desktopPushEnabled)
    notifications = new DesktopPushDecorator(notifications);

// client just have to interact with the last decorator
  notifications.send("This is trial notification");
}

run();

Output

image.png

When To Use Decorator Design Pattern

  • When you need to add and remove extra behaviors to object at runtime.
  • When inheritance will make the code too complex and awkward.

Advantages

  • Object's behavior can be extended without any complexity and without changing other functionality's code.
  • Any functionality can be added or removed at runtime.
  • You don't have to build a monolith class having all functionalities in it making the class complex and difficult for extensibility.

Disadvantages

  • Removing a wrapper from stack is hard.
  • The initial configuration code might look ugly.

That's a wrap guys. I hope you got what decorator pattern is, how and when to implement it.

Follow me on, Twitter, GitHub and LinkedIn .