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)
I think these are some pretty poor examples of efficient decorator use.
A common actual use-case is caching:
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.
Nice one. Decorators are all the more relevant now that POP is in favour ✌️