Decorators

A function decorator accepts a function, wraps (or decorates) it’s call and returns the wrapper, which alters default behavior.

For example, a checkPermissionDecorator decorator may only allow the function to run if the user has enough permissions for that.

function checkPermissionDecorator(f) {
  return function() {
    if (user.isAdmin()) f() 
    else alert('Not an admin yet')
  }
}

// Usage: make save check permissions

save = checkPermissionDecorator(save)

// Now save() calls will check permissions

Let’s see a more complex decorator. The doublingDecorator accepts a function func and returns the decorated variant which doubles the result:

function doublingDecorator(f) {        // (1)
  return function() {
    return 2*f.apply(this, arguments)
  } 
}

// Usage:

function sum(a, b) {
  return a + b
}

var doubleSum = doublingDecorator(sum)          // (2)

alert( doubleSum(1,2) ) // 6
alert( doubleSum(2,3) ) // 10

The decorator creates a new anonymous function which passes the call to func and doubles the result. By the way, this func.apply(this, arguments) trick is often used in JavaScript to forward the call in same context with same arguments.

Decorators is a great pattern of programming, because it allows to take an existing function and extend/modify it’s behavior.

There are two reasons why decorators are cool:

  1. Decorators can be reused. The doublingDecorator can be applied to minus and divide as well as sum.
  2. Multiple decorators can be combined for greater flexibility.

See the tasks below for more examples.

Create a function makeLogging(f) which takes an arbitrary function f, and makes a wrapper over it which logs calls. The wrapper should have a static outputLog() method to output the log.

Should work like this:

function work(a,b) { /* arbitrary function */ }

function makeLogging(f) { /* your code */ }

work = makeLogging(work)

// now work should log it's calls somewhere (but not in global)
work(1,2)
work(5,6)
work.outputLog() // <-- should alert('1,2'), alert('5,6')

No modifications of work are allowed. Your code should reside only in makeLogging.

Open solution
Solution

The idea of solution is given in the task.

We make a wrapper function which puts arguments into the log and forwards the call to f.

See the solution, and comments below it.

function work(a,b) { /*...*/ }

function makeLogging(f) { 
  var log = []  // (1)

  function wrapper() {
    log.push(arguments)
    return f.apply(this, arguments)   // (2)
  }

  wrapper.outputLog = function() {  
    for(var i=0; i<log.length; i++) {
      alert( [].join.call(log[i], ',') ) // (3)
    }
  }

  return wrapper
}

work = makeLogging(work)

work(1, 10) 
work(2, 20)
work.outputLog()

The details:

  1. The log is implemented via closure.
  2. Log and forward the call, including this. So, if work is an object method, everything is still fine.
  3. We log arguments, which is not an array. So, we borrow join from arrays.

Using a wrapper gives a nasty side effect. All static methods of the function can not be accessed any more, because they are on the function, which is wrapped around:

work.a = 5
work = makeLogging(work)
alert(work.a) // undefined

So, static methods and functional decorators are not friends.


Create a function makeCaching(f) which takes a one-argument function f(arg), and makes a wrapper over it which caches calls.

The wrapper should have a static flush() method to flush the cache.

Function f is allowed to have only one argument.

Should work like this:

function work(arg) { return Math.random()*arg }

function makeCaching(f) { /* your code */ }

work = makeCaching(work);

var a = work(1);
var b = work(1);
alert( a == b ) // true (cached)

work.flush()    // clears the cache

b = work(1)
alert( a == b ) // false

No modifications of work are allowed. Your code should reside only in makeCaching.

Open solution
Solution

The idea of solution is given in the task.

We make a wrapper function which puts arguments into the log and forwards the call to f.

See the solution, and comments below it.

function work(arg) { return Math.random()*arg }

function makeCaching(f) { 
  var cache = {};  

  function wrapper(arg) {
    if (!(arg in cache)) {   // (1)
      cache[arg] = f.call(this, arg);
    }
    return cache[arg];
  }

  wrapper.flush = function() {  
    cache = {};
  }

  return wrapper;
}

work = makeCaching(work);

alert( work(1) );
alert( work(1) ); // outputs same
work.flush();      
alert( work(1) ); // output changed

The object keeps the cache. The value obj[arg] can be anything, even undefined, that’s why we are using the arg in obj test instead of obj[arg] !== undefined.

Tutorial

Donate

Donate to this project