Problems with static inheritance

[originally posted 2014-03-27]

Some time in 2013 as I was working through some JavaScript constructor inheritance tests, I found an example of static inheritance as an anti-pattern (meaning don’t bother with it) in the transpiled code from a CoffeeScript tutorial.

(31 July 2015: For the record, I no longer use the ‘inheritance via super’ concept in JavaScript.)

The example is taken from Programming in CoffeeScript by Mark Bates, Addison-Wesley, pp. 147-150, where the author shows that CoffeeScript does not support static inheritance through the __super__ keyword.

However, the example contains more fundamental problems not specific to CoffeeScript, as I’ll show with my own constructor.js examples.

Here’s the CoffeeScript code *

    class Employee

      constructor: ->
        Employee.hire @

      @hire: (employee) ->
        @allEmployees ||= []
        @allEmployees.push employee

      @total: (employee) ->
        console.log "there are #{@allEmployees.length} employees."
        @allEmployees.length

    class Manager extends Employee

JavaScript generated by the transpiler

var Employee, Manager,
  __hasProp = {}.hasOwnProperty,
  __extends = function(child, parent) { 
    for (var key in parent) { 
      if (__hasProp.call(parent, key)) child[key] = parent[key]; 
    } 
    function ctor() { this.constructor = child; } 
    ctor.prototype = parent.prototype; 
    child.prototype = new ctor(); 
    child.__super__ = parent.prototype; 
    return child; };

Employee = (function() {
  function Employee() {
    Employee.hire(this);
  }

  Employee.hire = function(employee) {
    this.allEmployees || (this.allEmployees = []);
    return this.allEmployees.push(employee);
  };

  Employee.total = function(employee) {
    console.log("there are " + this.allEmployees.length + " employees.");
    return this.allEmployees.length;
  };

  return Employee;

})();

Manager = (function(_super) {
  __extends(Manager, _super);

  function Manager() {
    return Manager.__super__.constructor.apply(this, arguments);
  }

  return Manager;

})(Employee);

After calling new Employee() three times, Employee.total() returns 3.
After calling new Manager() once, , Employee.total() returns 4.

Add the static inheritance

The author then adds a static Manager.total method which is meant to inherit-and-override the Employee.total method

    class Manager extends Employee

      @total: -> 
        console.log "0 managers"
        super

generating this JavaScript by transpiler

Manager.total = function total() {
  return Manager.__super__.constructor.total.apply(this, arguments);
};

But the call to Manager.total() throws an error

TypeError: this.allEmployees is undefined !

The problem here is that the this being applied to the super constructor’s total() method refers to the Manager constructor rather than a Manager instance. CoffeeScript’s __extends method makes no attempt to determine where super is referenced, assuming that ANY calls to a super apply to this. And because Manager does NOT inherit the static allEmployees array from Employee, applying this to total(), rather than just calling total() directly, produces the error.

Replacing this manually with either Employee or Manager.__super__.constructor clears up this problem

return Manager.__super__.constructor.total.apply(Employee, arguments);
return Manager.__super__.constructor.total.apply(Manager.__super__.constructor, arguments);

but hand-cleaning CoffeeScript-generated JavaScript is not a winning scalable strategy.

But in constructor.js, this isn’t this

While this specific example reveals a limitation in CoffeeScript, I found there was another flaw in the example when trying this out on constructor.js.

background

In constructor.js, an inheriting constructor makes a call to this.__super__() that creates an instance of the super constructor, sets it to __super__ on the inheriting object, and copies properties in the inheriting object. All further references to __super__ point to the super object rather than a super constructor.

Back to our problem…

Here’s an Employee and Manager test fixture for constructor.js

function Employee() {   
  Employee.hire(this);
}

Employee.hire = function hire(employee) {
  this.employees || (this.employees = []);
  return this.employees.push(employee);
};

Employee.total = function total() {
  return this.employees.length;
};

var Manager = Constructor.extend(Employee, {
  constructor: function Manager() {    
    this.__super__(); // here's our constructor calling super 
  }
});

Manager.total = function total() {
  return this.__super__.constructor.total(); // super_object method
};

After calling new Employee() three times, Employee.total() returns 3.
After calling new Manager() once, , Employee.total() returns 4.

However, when I iterated over the Employee.employees array, the expected Manager entry at the end was NOT a Manager at all, but an Employee.

t.strictEqual(Employee.employees[1].constructor, Manager); // ?! not ok !?

It turns out the culprit is simply in the Employee constructor, which takes no data, but passes its current scope (whatever this refers to) along to its static hire() method:

  function Employee() {
    Employee.hire(this);
  }

Adding some logging to each constructor reveals the problem. This is OK:

function Manager() {
  console.log(this.constructor.name) // Manager
  this.__super__();
}

But this call to this.__super__() in Manager always delegates to the super instance of Employee:

function Employee() {
  console.log(this.constructor.name) // Employee !
  Employee.hire(this);
}

…which passes itself to Employee.hire. This means that my __super__ implementation is doing work behind the scenes with two different objects rather than a unified one.

fixing “static” “inheritance” for constructor.js

To get things to work in this scenario, avoid the work in the constructor, and make a prototype method hire() in Employee, and move the Employee.hire(this); statement from the constructor to the hire() method:

Employee.prototype.hire = function() {
  Employee.hire(this);
};

…and in Manager, we can override that new method and access the Manager instance’s __super__ object and call hire.apply() passing the Manager instance as the scope object to set the this reference properly:

Manager.prototype.hire = function() {
  this.__super__.hire.apply(this);
};

Some test invocations later…

Call hire on new instances of each type:

(new Employee()).hire();
(new Manager()).hire();

t.strictEqual(Employee.total(), 2, 'should be 2 employees');
t.strictEqual(Manager.total(), Employee.total(), 'should inherit total()');

then test that Employee.employees[Employee.employees - 1] correctly returns a Manager instance:

t.strictEqual(Employee.employees[1].constructor, Manager, 'should be Manager');

fixing “static” “inheritance” in CoffeeScript

We’ll do the same thing as with constructor.js. Move Employee.hire(this) to a new prototype method named hire, and override Manager.prototype.hire to call its super instance object’s hire method, then replace the super call in Manager.@total with a direct call to Manager.super.constructor.total()`:

    class Employee

      constructor: ->
        #Employee.hire @

      hire: ->
        Employee.hire(this)

    ///

    class Manager extends Employee

      @total: ->
        console.log "call to super should error"
        Manager.__super__.constructor.total()

      hire: ->
        super

These generate

function Employee() {}

Employee.prototype.hire = function() {
  return Employee.hire(this);
};

///

function Manager() {
  return Manager.__super__.constructor.apply(this, arguments);
}

Manager.total = function() {
  console.log("call to super should error");
  return Manager.__super__.constructor.total();
};

Manager.prototype.hire = function() {
  return Manager.__super__.hire.apply(this, arguments);
};

This works, but it’s not ideal. The employees array really should be owned by a collection handler rather be tacked on to a constructor — because JavaScript constructors are not classes. That’s probably what led Jeremy Ashkenas to make Backbone Collections independent of the Model, View and Controller(Router) constructors.

nightcap recap

  • avoid use of super in “static” methods in CoffeeScript
  • avoid “static” calls in the constructor
  • move super calls from the constructor to prototype methods
  • make super calls from a static method point to a static method on
    __super__.constructor
  • (31 July 2015) I no longer use ‘super’ in JavaScript

fin

*

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s