Using Decorators to Declaratively Extend Functions

The decorator pattern gained fame in the object-oriented world after being featured in the classic 1994 Gang of Four book, Design Patterns. Since then, it’s been used extensively in traditional object-oriented programming as an alternative to inheritance. What’s really going on behind the scenes though, is composition, which means decorators are also great for cleaning up some functional programming boilerplate.

We’re going to look at decorators in JavaScript, along with some proposed syntax sugar coming in ES2017 that will make it easier to use them.

What Is a Decorator?

At a high level, decorators are a way of extending the functionality of a class or a function without subclassing or modifying the underlying class or function. They allow you to express a “has a” relationship instead of an “is a” relationship, which would be expressed more naturally with inheritance.

The old objected-oriented cliche of building a car provides a convenient example. A car can be built with any number of optional packages or upgrades, which provide the car with new attributes and behaviors. Trying to express this with subclasses would get unwieldy very quickly, as you would need a subclass to represent every possible combination. This is where the decorator pattern comes in.

How Do Decorators Work?

A decorator is just a class that wraps another class, or a function that wraps another function, and adds some additional functionality. The Underscore and Lodash libraries provide several decorators, although they aren’t labeled as such.

_.debounce, _.memoize and others like them are all functions that follow the decorator pattern. They take as input a function that you’ve built, and return to you another function that works slightly differently, without modifying the original function.

One of the simplest examples of a useful decorator is one which logs some information when a function runs, without cluttering up the function itself with logging statements. Say we have a function add that simply adds two numbers, and we want to log both arguments before computing the sum. We could modify the add function directly and simply log the arguments before returning the sum, but this will fundamentally change the behavior of the add function.

We can implement the decorator pattern in ES5 land by making a wrapper function called log and composing it with our add function. Then, whenever we want the logging version of the function, we can call logAdd instead of log and we will get our logging statements without having to modify the original add function.


function add(x, y) {
  return x + y
}

function log(fn) {
  return function() {
    console.log("the function was called with " + [].slice.apply(arguments));
    return fn.apply(this, arguments);
  }
};

var logAdd = log(add);
logAdd(1,2) //Prints the arguments and then the result

Decorators as a Language Feature

Decorators are special-purpose functions designed to be used in a very specific way. They’ve been given special syntax in many languages, and it’s coming to JavaScript in ES2017. Unfortunately, function decorators aren’t part of the proposal, so they will only be able to be used with methods and classes. However, the pattern itself can always be used instead.

If the above function is in a class, then in the current draft form, that example can be written like this:


//First define the decorator
//target is the class on which you apply the decorator, or the class of the method on which you apply the decorator.
//key is the property name of the specific property you are applying the decorator to.
//descriptor is the descriptor of the property.
const log = (target, key, descriptor) => {
  const fn = descriptor.value;
  descriptor.value = (...args) => {
    console.log("the function was called with " + args);
    return fn.apply(this, ...args);
  }
}

//Then apply it to an a method.
class MyMath {
  @log
  add(x, y) {
   return x + y;
  }
}

This allows us to abstract away the awkward function wrapping.

Automatically Binding Functions

A common pattern in React components is binding the event handlers in the constructor:


class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    //Bind the click handler here unless you want 'this' bound to 'window'.
    this.clickHandler = this.clickHandler.bind(this);
  }

  clickHandler() {
    //Do something with 'this'
    console.log(this);
  }

  render() {
    return ()
  }
}

So each time you write a function that’s going to handle events, you have to remember to go to the constructor and bind it. That’s kind of a drag. It’s simple to write a decorator to do this work for you:


const bind = (target, key, descriptor) {
  descriptor.value = descriptor.value.bind(target);
}

Then, in your component, you can get rid of the code in your constructor and just annotate the function that you want to bind:


@bind
clickHandler() {
  //Do something with 'this'
  console.log(this);
}

Stacking Decorators

To compose decorators, you can just stack them. The previous two examples can be combined to produce a function that is bound to its component and logs its arguments.


@bind
@log
clickHandler() {
  //Do something with 'this'
  console.log(this);
}

They are executed from the bottom up. You shouldn’t ever have to think about that, though. If you’re writing decorators that have to be applied in a specific order, it might not be the right abstraction. It’s also possible to write decorators that are dependent on other decorators that have been applied before them, but this is also a sign that there might be a better way to express the idea.

However, when used appropriately, decorators can give you a nice, declarative way to modify the runtime behavior of your functions.

Check out the draft proposal at tc39.

Conversation
  • Harry Liu says:

    return fn.apply(this, …args);

    should be: return fn.apply(this, args); ?

  • Harry Liu says:

    Nice article!

  • Comments are closed.