DEV Community

karimAlaa
karimAlaa

Posted on

Decorators and Code Reuse

Decorators is a design pattern that helps in code reuse. It allows behavior to be added/extended to an existing object dynamically. The idea is that the decoration itself isn't essential to the base functionality of an object.

What is code reuse?

Short answer is to DRY (Don't Repeat Yourself) out your code by not have any duplicate logic in different location. It's a path to generalize code aimed at similar goals. It's good practice to always take notice of similar parts of your code and to take action by refactoring that part to a more reusable code.

Let's take an example:

Let's take a simple example of a game where your character is a warrior that moves left, right and can jump. We'll use various object oriented concept in this example.

Let's start with simply defining our warriors as objects. We will define the first warrior as darkWarrior

var darkWarrior = {};

We will add a property location to our darkWarrior to store the position of the character.

var darkWarrior = {
  location: 1,
  weapon: "Sword"
};

Now assume that at one point in time your character will need to move. We will need to increment the location property in our darkWarrior to reflect that.

var darkWarrior = {
  location: 1,
  weapon: "Sword"
};

darkWarrior.location+=1

Now let's add another lightWarrior warrior

var lightWarrior = {
  location: 7,
  weapon: "Spear"
};

lightWarrior.location+=1

We've started to repeat ourselves now with this new warrior. While this is a very simple code, you already see the need to DRY out the code somehow. First question to ask is: which part of the code needs to be refactored?

Well, for our case we can see that we increment the location property at two different places.

One way to refactor this is by taking incrementation behavior into a separate function, like so:

var moveRight= function(warrior){
  warrior.location+=1
}

var darkWarrior = {
  location: 1,
  weapon: "Sword"
};

moveRight(darkWarrior);

var lightWarrior = {
  location: 7,
  weapon: "Spear"
};

moveRight(lightWarrior);

This refactor helps in writing your logic only once without unnecessary repetition. Plus this makes your code more maintainable by having one place to update your changes if needed. For example, if you were to change the location count to increment by 10 instead of 1, in the old case we would have to change it in two places but after the refactor we will only need to update the moveRight function.

Let's also pass the moveRight function to be a method in our warriors objects.

var moveRight= function(warrior){
  warrior.location+=1
}

var darkWarrior = {
  location: 1,
  weapon: "Sword",
  moveRight: moveRight
};

var lightWarrior = {
  location: 7,
  weapon: "Spear",
  moveRight: moveRight
};

In that case we don't have to pass an object to the moveRight function. We can actually use this to reference the object invoking the method.

var moveRight= function(){
  this.location+=1
}

var darkWarrior = {
  location: 1,
  weapon: "Sword",
  moveRight: moveRight
};

var lightWarrior = {
  location: 7,
  weapon: "Spear",
  moveRight: moveRight
};

One problem with this approach is the this keyword is based on the calling context. For example if we want our warrior to move to the right every 1 second, we will need to pass the moveRight function to an interval timeout function like so:

setInterval(lightWarrior.moveRight, 1000);

Decorators

In the case above, this will point to the global window object and not to the calling object. What we can do is create a wrapper function that takes our object and extends it with the moveRight function


var darkWarrior = {
  location: 1,
  weapon: "Sword",
};

var lightWarrior = {
  location: 7,
  weapon: "Spear",
};

var moveAdder=function(warrior){
  warrior.moveRigt=function(){
    warrior.location+=1
  }
  return warrior
}

darkWarrior  = moveAdder(darkWarrior)
lightWarrior = moveAdder(lightWarrior)

We call this approach a decorator pattern where we used a function that has only one purpose, which is to add a new behavior to our warrior. We also used the closure concept by replacing this with the actual reference to the object passed to the decorator function

We can also extend this concept by not just adding functionality but also altering existing functionality. Let's extend the previous example of our darkWarrior being faster than the other.

var moveFaster=function(warrior){
  warrior.moveRigt=function(){
    warrior.location+=8
  }
  return warrior
}

darkWarrior.moveRight() //darkWarrior.location = 2

darkWarrior = moveFaster(darkWarrior)

darkWarrior.moveRight() //darkWarrior.location = 10

That's it for Decorators.

Top comments (2)

Collapse
 
erebos-manannan profile image
Erebos Manannán • Edited

I think these are some pretty poor examples of efficient decorator use.

A common actual use-case is caching:

function getItem(id) {
    console.log("Querying database for item " + id)
    return database.query({id: id}) // Any heavy operation
}

function cached(scope, fn) {
    var _cache = {};
    return function() {
        var key = JSON.stringify(arguments)
        if (!_cache[key]) {
            _cache[key] = fn.apply(scope, arguments)
        }

        return _cache[key]
    }   
}

itemCached = cached(undefined, getItem)

console.log(itemCached(1))
console.log(itemCached(2))
console.log(itemCached(1))

Another easy example I can think of is "promisifying" Node.js functions, i.e. changing the functions that take a callback function to use promises instead.

var fs = require('fs')

function promisify(scope, fn) {
    return function() {
        var args = Array.prototype.slice.apply(arguments)
        return new Promise(function(resolve, reject) {
            args.push(function(err, result) {
                if (err) {
                    reject(err)
                } else {
                    resolve(result)
                }
            })
            fn.apply(scope, args)
        })
    }   
}

var readFilePromise = promisify(fs, fs.readFile)
readFilePromise("test.json").then(function(result) {
    console.log(result.toString('utf8'))
}).catch(function(err) {
    throw err
})
Collapse
 
eonist profile image
Eon

Nice one. Decorators are all the more relevant now that POP is in favour ✌️