DailyJS

Let's Make a Framework: OO Part 2

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks web lmaf

Let's Make a Framework: OO Part 2

Posted by Alex R. Young on .
Featured

tutorials frameworks web lmaf

Let's Make a Framework: OO Part 2

Posted by Alex R. Young on .

Welcome to part 3 of Let's Make a Framework wherein I discuss building
a JavaScript framework suited to both command line and web development.
This part completes the discussion of object oriented JavaScript
support. In the last part, Classes, Inheritance,
Extend
I talked
about Prototypal inheritance and classical object models. This part will
conclude the discussion, explaining more about how
turing.oo works, and will explore super().

If you haven't been following along, I've been tagging these articles
with lmaf so you can find them. The
project we're creating is called Turing and is available on GitHub:
turing.js.

Class Creation in More Depth

Last week I was parading around initialize as if you were
intimately familiar with prototype.js or Ruby. I apologise for that. In
case I confused you, all you need to know is initialize is
our way of saying call this method when you set up my class.

Turing's Class.create method sets up classes. During the
setup it defines a function that will be called when the class is
instantiated. So when you say new, it will run
initialize. The code behind this is very simple:

  create: function() {
    var methods = null,
        parent  = undefined,
        klass   = function() {
          this.initialize.apply(this, arguments);
        };

I sometimes feel like apply is magical, but it's not really
magic in the negative programmer sense of the word -- it doesn't hide too much from you. In this case it just calls your
initialize method against the newly created class using the
supplied arguments. Again, the arguments variable seems
like magic... but that's just a helpful variable JavaScript makes
available when a function is called.

Because I've made this contract -- that all objects will have initialize
methods -- we need to define one in cases where classes don't specify
initialize:

    if (!klass.prototype.initialize)
      klass.prototype.initialize = function(){};

Syntax Sugar * Extend === Mixin

It would be cool to be able to mix other object prototypes into our
class. Ruby does this and I've often found it useful. The syntax could
look like this:

var MixinUser = turing.Class({
  include: User,

  initialize: function(log) {
    this.log = log;
  }
});

Mixins should have some simple rules to ensure the resulting object
compositions are sane:

  • Methods should be included from the specified classes
  • The initialize method should not be overwritten
  • Multiple includes should be possible

Since our classes are being run through turing.oo.create,
we can easily look for an include property and include
methods as required. Rather than including the bulk of this code in
create, it should be in another mixin method
in turing.oo to keep create readable.

To satisfy the rules above (which I've turned into unit tests), this
pseudo-code is required:

  mixin: function(klass, things) {
    if "there are some valid things" {
      if "the things are a class" {
        "use turing.oo.extend to copy the methods over"
      } else if "the things are an array" {
        for "each class in the array" {
          "use turing.oo.extend to copy the methods over"
        }
      }
    }
  },

Super

Last week I used an example that featured
prototype.js's super handling. Prototype allows classes to be extended using addMethods so
it can track which methods have been added and provide access to
overriden methods using \$super.

I want to be able to inherit from a class and call methods that I
override. Given a User class (from the
fixtures/example_classes.js file), we can inherit from it to make a SuperUser:

var User = turing.Class({
  initialize: function(name, age) {
    this.name = name;
    this.age  = age;
  },

  login: function() {
    return true;
  },

  toString: function() {
    return "name: " + this.name + ", age: " + this.age;
  }
});

var SuperUser = turing.Class(User, {
  initialize: function() {
    // Somehow call the parent's initialize
  }
});

A test to make sure the parent's initialize gets called is
simple enough:

  given('an inherited class that uses super', function() {
    var superUser = new SuperUser('alex', 104);
    should('have run super()', superUser.age).equals(104);
  });

If I run this without a super implementation, I get this:

Given an inherited class that uses super
  - should have run super(): 104 does not equal: undefined

To fix this all we need to do is call a previously defined method. Hey,
it's time for apply again!

var SuperUser = turing.Class(User, {
  initialize: function() {
    User.prototype.initialize.apply(this, arguments);
  }
});

This isn't perfect though. Most languages make their super
implementation simpler for the caller -- forcing people to use
apply like this is unwieldy. One way around this is to make
the parent class prototype available and add a super method to the
class. The super method can simply use apply in the same
manner. The only downside is you have to specify the method name:

var SuperUser = turing.Class(User, {
  initialize: function() {
    this.$super('initialize', arguments);
  },

  toString: function() {
    return "SuperUser: " + this.$super('toString');
  }
});

This is simple, lightweight, and easy to understand. The method name
could be inferred by other means, but this would complicate the library
beyond the scope of this article (meaning if you can do it and make it
cross-browser then cool!)

Conclusion

Now we've got a simple, readable OO class. This will allow us to
structure other parts of Turing in a reusable way. I hope the last two
articles have demonstrated that JavaScript's simplicity allows you to
define your own behaviour for things that feel like fundamental language
features.