Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

file 267 lines (185 sloc) 10.394 kb

A Quick Guide to Using YouAreDaChef

What YouAreDaChef Does

YouAreDaChef is a utility for meta-programming Object-Oriented JavaScript. YouAreDaChef supports modifying JavaScript that is written in either a prototype style or a class-oriented style. JavaScript supports extending and redefining methods out of the box. YouAreDaChef takes this further by also supporting Aspect-Oriented Programming, specifically "decorating" methods with before, after, around, and guard advice.

This is almost exactly like controller filters and ActiveRecord callbacks in Ruby on Rails, but for your JavaScript and Backbone objects!

Aspect-Oriented Programming is a code organization practice. Its purpose is to separate concerns by responsibility, even when the implementation of that responsibility spans multiple classes or has finer granularity than a method. AOP seeks to avoid tangling multiple responsibilities in a single class or method body as well as to avoid scattering a single responsibility across several classes.

YouAreDaChef operates on classes constructed in CoffeeScript with the class keyword like this:

class Muppet
    name: -> @_name

class Animal extends Muppet
    constructor: (@_name) ->
    instrument: -> 'drums'

YouAreDaChef also operates on functions with prototype chains hand-rolled in standard JavaScript like this:

var Animal, Muppet;

Muppet = function() {};

Muppet.prototype.name = function() {
  return this._name;
};

Animal = function(_name) {
  this._name = _name;
};

Animal.prototype = new Muppet();

Animal.prototype.instrument = function() {
  return 'drums';
};

In this document, the words "class" and "method" will be used regardless of how you choose to implement inheritance, and the examples will alternate between CoffeeScript and JavaScript.

Basic Syntax

Using YouAreDaChef resembles using jQuery. You start with YouAreDaChef and then specify what you wish to "advise." In Café au Life, the class Square has a method called set_memo and another called initialize, something like this:

class Square
    initialize: (args...) ->
        # ...
    set_memo: (index, square) ->
        @memoized[index] = square

In the garbage collection module, YouAreDaChef decorates these methods with before and after advice:

    YouAreDaChef

      .clazz(Square)

        .method('initialize')
          .after ->
            @references = 0

        .method('set_memo')
          .before (index) ->
            if (existing = @get_memo(index))
              existing.decrementReference()
          .after (index, square) ->
            square.incrementReference()

As a result, when a Square's set_memo method is called, its "before" advice is executed, then its body or "default" advice is executed, then its "after" advice is executed. If you define more than one before or after advice, they will all be executed in order. Before and after advice is passed the same parameters as the 'default' behaviour but is executed for its side-effects only. There is no way to alter the parameters passed to the default behaviour or to modify its return value.

Note: If you are using inheritance, YouAreDaChef arranges things such that all of the before advice is executed, including the inherited advice, but only the most specific method body or "default" is evaluated. In many cases, you can avoid trying to use a super method in a subclass by defining method advice in the subclass instead. This makes your intent easier to understand.

Some Shortcuts

When you only want to provide one piece of advice to one method, you can use 'compact' syntax for specifying the method name as a parameter of the advice:

YouAreDaChef
  .clazz(Square)
    .after('initialize', function() {
      this.references = 0;
    });

You can also save yourself a line of code by treating YouAreDaChef as a function (just like $(...) in jQuery) and specifying one or more classes as parameters:

YouAreDaChef(Square)
  .after 'initialize', ->
    @references = 0

You can specify more than one class or more than one method. If you are a stickler for English-y method names, you can call either .clazz or .clazzes and .method or .methods, like this:

var debug_id = 0;

YouAreDaChef
  .clazzes(Square, Cell)
    .after('initialize', function() {
      debug_id = debug_id + 1;
      this.debug_id = debug_id;
    });

And when you want to hand out a lot of the same kind of advice, pass in a hash of methods and advices:

    YouAreDaChef

      .clazz(Square)

        .before
          set_memo: (index) ->
            if (existing = @get_memo(index))
              existing.decrementReference()

        .after
          initialize: ->
            @references = 0
          set_memo: (index, square) ->
            square.incrementReference()

This is equivalent to the example for basic syntax, just organized differently. The purpose of YouAreDaChef is to give you ways to organize your code by responsibility. Thus, it provides multiple ways to declare your advice.

Finally, you can chain as much or as little of your advice together as you want. Just as you can chain advice for different methods or different kinds of advice, you can chain different classes:

var debug_id = 0;

YouAreDaChef
  .clazz(Cell)
    .after('initialize', function() {
      debug_id = debug_id + 1;
      this.debug_id = "C" + debug_id;
    });
  .clazz(Square)
    .after('initialize', function() {
      debug_id = debug_id + 1;
      this.debug_id = "S" + debug_id;
    });

Regular Expressions

Instead of using a method name (or list of method names), you can supply a regular expression to specify the method(s) to be advised. The match data will be passed as the first parameter, so it is possible to use match groups in your advice:

EnterpriseyLegume = (function() {

  function EnterpriseyLegume() {}

  EnterpriseyLegume.prototype.setId = function(id) {
    this.id = id;
  };

  EnterpriseyLegume.prototype.setName = function(name) {
    this.name = name;
  };

  EnterpriseyLegume.prototype.setDepartment = function(department) {
    this.department = department;
  };

  EnterpriseyLegume.prototype.setCostCentre = function(costCentre) {
    this.costCentre = costCentre;
  };

  return EnterpriseyLegume;

})();

YouAreDaChef(EnterpriseyLegume)
  .methods(/set(.*)/)
    .after(function(match, value) {
      writeToLog("" + match[1] + " set to: " + value);
    });

Around Advice

Before and after advice are executed for side-effects only. "Around" advice is all-encompassing: It is passed the parameters and the default advice, and it takes care of calling the default advice and deciding what to return. Here is some fictional code that wraps some methods up in a transaction:

YouAreDaChef(EnterpriseyLegume)
  .around /set(.*)/, (default_fn, match, value) ->
    performTransaction ->
      default_fn(value)

When multiple pieces of around advice are provided, they nest.

Guard Advice

Ruby on Rails users are familiar with controller filters. YouAreDaChef's before, after, and around advices are like controller filters. Likewise, Ruby on Rails provides callbacks for ActiveRecord models. YouAreDaChef works just like the callback queues, every class inherits the before, after, and around advice of its superclasses (or prototypes).

Well, YouAreDaChef works almost like Ruby on Rails. The one difference is that YouAreDaChef doesn't care what before advice returns. false, null, undefined, it doesn't matter and it doesn't halt the function call. In Rails, if a before filter returns something falsey, the request cycle is halted.

The behaviour in YouAreDaChef is consistent: before advice is stuff you want to do before the method call, after advice is stuff you want to do after the method call. Always. If you forget to pay attention to what your before advice returns, it won't break your method. But what if you actually want a filter? In that case, you use guard advice:

YouAreDaChef(EnterpriseyLegume)
  .when('setId', function(value) {
    return !isNaN(value);
  });

setId will be aborted if the value passed is not an integer. This pattern of testing for an error and returning the negation of the test value is common, so YouAreDaChef provides a shortcut:

YouAreDaChef(EnterpriseyLegume)
  .unless 'setId', (value) -> isNaN(value) # Equivalent to isNaN

This has the exact same semantics as the .when advice, .setId will be evaluated unless the value is not a number.

Installation

npm install YouAreDaChef

Or:

git clone git://github.com/raganwald/YouAreDaChef.git

Or:

curl -o YouAreDaChef.zip https://nodeload.github.com/raganwald/YouAreDaChef/zipball/master

Where can I read more?

YouAreDaChef README
Separating Concerns in Coffeescript using Aspect-Oriented Programming
Implementing Garbage Collection in CS/JS with Aspect-Oriented Programming

Et cetera

YouAreDaChef was created by Reg "raganwald" Braithwaite. It is available under the terms of the MIT License.

Something went wrong with that request. Please try again.