2013/01/28

Doing OO - the JS Way!

I was fighting to understand how to do object-oriented design with Javascript for years and this week I finally think I'm onto something.

I keep seeing the same scenario almost every time I start a new project:
* Start simple, customer says. We don't need fancy UI, customer says. Just make it work on the back-end, let's not worry about async-shmasync.
* First delivery - beautiful, they say. But can we make a bit more pop-y, they say. Let's start by auto-updating the grid "dashboard" every 5 seconds.

So I start building a PageManager (I know, don't let me get started on the name). At first, it's only retrieving the dashboard. Then, I need to register any number of "widgets" that need to be updated async. After that - modal "dialogs", on-demand script loading, etc. It all goes (relatively) well until the third or fourth sprint where it becomes clear that it's all good and everything, but we need to do it a little bit differently in one specific section of the application.

And it was my brain-twister for a long time - how do I get to reuse what I already had in this PageManager and add on it or change it completely. In other words - how do I get to do my inheritance and overriding in Javascript? Is there even a way to do it?

Turns out there is. And it lies in the magic property of every object that is called prototype.

Now, when I set off to learn "proper" Javascript, I decided to take a classic problem and to use it to build a working solution. I chose Conway's Game of Life because it is simple enough to understand and design and yet gives me plenty of possibilities to experiment and learn.

So, the Game of Life consists of a rectangular plane where each cell is either dead or alive. This concept can be easily modeled with a simple class which has one boolean variable indicating whether the cell is dead or alive.

var Cell = function (isAlive) {
    var _isAlive = isAlive != undefined ? isAlive : false;

    this.IsAlive = function () {
        return _isAlive;
    };

    this.IsDead = function () {
        return !_isAlive;
    };
};

That looks pretty well and it works too. You can create new Cells, passing them the initial state. And when you ask them back whether they are dead or alive, they respond properly. Go on and try it now, if you want.

What if I want to create an alternate universe where the rules of the game remain the same:
- a living cell with 2 or 3 alive neighbors survives in the next generation
- a dead cell with exactly 3 alive neighbors springs to life

But in my universe once a dead cell is resurrected, it will be neither dead nor alive. It will be a Zombie, who can never die but will not count as alive when counting the poor souls.

Knowing a bit about the Javascript call hierarchy, my first attempt was this very simple code:

var ZombieCell = function () {

    this.IsDead = function () {
        return _isAlive;
    };
};

ZombieCell.prototype = new Cell();

Nothing too complicated, and it looks fine. With only one drawback - it doesn't actually work.
The first problem is that the _isAlive field is not visible from ZombieCell. Because it is, well, private!
The other problem is that the constructor logic is never executed. Not when you set the prototype and not even when you create zombie cells.

After a lot of experimenting and trying to find the best syntactic support (there isn't any), this is what I came up with.

var ZombieCell = function () {
    Cell.call(this, false);

    this.IsDead = function () {
        return false;
    };
};

ZombieCell.prototype = Object.create(Cell);

A few things worth noting:
- the prototype property has to point to a valid "instance". Since my base class has constructor parameters, I had to use Object.create() to get one
- although the prototype points to a valid base class instance, the construction logic is NOT inherited and has to be explicitly invoked using call()
- ZombieCell cannot see the private variable _isAlive. Because it is exactly this - private, which means it is not visible by anything else but the entity that declares it.

1 comment:

  1. Good observation. To be honest, I tested only in Chrome, I am relying on unit tests though.

    ReplyDelete