Functional object-orientation in Javascript

Idea from "Javascript The Good Parts" (Crockford)

http://semicolonreport.blogspot.com

Henrik Hjelte
Comment on this on the blogpost
Javascript logging comes here
Functional object-oriented programming in Javascript
====================================================

This is a short tutorial on the best and easiest way to do
object-oriented programming in javascript. It is the method described
as "functional" by Douglas Crockford in "Javascript the good parts",
the chapter on inheritance. All deep thinking was done by Crockford,
the only smart thing I have done is to understand to appreciate the
ideas. And I have made a new example, and possibly some new arguments
for the method. What is funny is that I like this method but not really
what Crockford points out as the killer argument for it (real privacy).

I won't go into detail why you should do it in this way instead of
traditional way. Here are my reasons, not Crockfords:

I don't need "this". I no longer get bugs because "this" is
wrong. 

I don't need "new". I don't like it and less is more. If you like
data-abstraction you probably agree that it is a good practice to have
functions to create instances instead of using new directly. This is
by coincidence the way to do it with functional OO.

I don't need an OO-framework to do OO conveniently. I don't want them,
this is better.

I seldom need call or apply. A normal javascript "method" needs its
"object". So it can not simply be used in functional programming,
unless you bind "this" correctly. But these methods are
self-contained, they know about the object without having to be called
'with' an object like call or apply. This makes for shorter, prettier
and less bug-prone code.

The example
===========

There is a short example of functional OO in Javascript the Good
Parts, but I belive this example is more concrete. It is the standard
OO-example with geometrical figures. You can calculate the area and draw
them on the webpage. In the example there is no real drawing going on,
but you can follow what is going on by following the logging. It is a
very dry example, and the logging is not very exciting, but I think it
is a good example.

Constructing instances
----------------------

Function that create an instance of an object is called makeXYZ.Every
make-function takes a spec(ification) object and an optional "my"
object, and returns an object. This is my name-convention, not from
Crockford.

The my-object
------------- 

The "my" object is because different make-functions might want to
share data, but not make it public. It should not normally be passed
into the make-function when you want to make an instance (see the
function test). Instead an empty object is created internally in
makeFilledCube. makeGeometricalFigure creates an object canvasDiv
(yes it could be a dom-node in a real-world example), and then adds
the canvasDiv to the my object. That way it can be accessed from any
other internal functions used by any of the other objects in the
inheritance chain. In this example the draw methods wants to use the
canvasDiv. 

The spec-object
---------------

The spec object should contain all the parameters you want to send in
when you create the object. For all objects you can send in color,
which makeGeometricalFigure stores in the common "my" object. Besides
color, the different geometrical figures wants different parameters, a
circle wants the diameter and a cube wants width. According to the
convention, all parameters should be in the spec object. You can not
use positional parameters. This creates better readability in a lot of
cases: If you see makeSquare(20,40) you might not know if 20 is height
or width. With makeSquare({height:20, width:40}) you do.

Inheritance
-----------

If you want an object to specialize another object (inheritance), you
create an instance of the object inside your "make" function. Then you
can change this instance. Add new functions (methods) and remove
functions. This is a very simple, dynamic and powerful. makeCircle
creates a genometrical figure, then adds an area function and a new
draw function. The draw function from geometrical figure is not
needed, so it is overwritten. But if you need it, you can do as in
makeFilledCube. First save away the draw function you get from
makeCube in a local variable, Then overwrite the draw method with a
new function, and this function in makeFilledCube can reach the
previous ("super") draw method from makeCube using the local
variable. Crockford suggests doing this in another way.

Object-orientation
------------------

The basic idea of object-orientation is that different functions are
executed automatically based on what type an object is. In contrast to
having one function that check the type with an if-statement and then
take different actions.

You can see in the function test, that figure.draw invokes a different
draw function for the red cube and the blue circle. So this is object
orientation. Just done in a different way.

Real privacy
------------

The functions you define inside the make-function can not be changed
from the outside, and if you call them from other functions defined
inside the make-function you have real private methods popular in C++
and Java. Crockford seems particularly fond of this feature. I am
not. I suggest the habit of exposing interesting "internal functions"
by returning an inner object called "internals" in your objects. In my
experience, for testing and other reasons real privacy is not a good
idea. The idea of protecting your perfect code from other less perfect
programmers is suspicious. It should suffice to discourage use of
internal methods, making it impossible to ever reach them is not a
good feature unless you are in the security business.

/*jslint  onevar: false*/
/*global $,document*/
// We use just a little bit of jquery 
// for the demo helpers

$(document).ready(function () {

    var log = (function () {
        var nr = 1;
        return function (text) {
            $('<p/>').text("" + nr + ": " + text).appendTo($('#log'));
            nr = nr + 1;
        };
    }());
    
    var someDrawingLib = {
        drawCircle: function () {
            log("drawCircle");
        },
        drawSquare: function () {
            log("drawSquare");
        },
        fill: function (aCanvas, aColor) {
            log("Fill on " + aCanvas + " in color " + aColor);
        }
    };
               
    function initializeCanvas() {
        log("Initialize Canvas.");
        return "some canvas";
    }

    function makeGeometricalFigure(spec, my) {
        log("makeGeometricalFigure");
        my = my || {};
        my.color = spec.color || "blue";

        my.canvasDiv = initializeCanvas();
        var draw = function () {
            throw ({message: "Abstract method"});
        };
        var api = {
            draw: draw
        };
        return api;
    }

    function makeCircle(spec, my) {
        log("makeCircle");
        my = my || {};
        var diameter = spec.diameter || 100;

        var me = makeGeometricalFigure(spec, my);
        var draw = function () {
            log("makeCircle.draw");             
            someDrawingLib.drawCircle(my.canvasDiv, diameter, my.color);
        };
        var area = function () {
            var radius = diameter / 2;
            return radius * radius * Math.PI;
        };
        me.draw = draw;
        me.area = area;
        return me;
    }

    function makeSquare(spec, my) {
        log("makeSquare");
        my = my || {};
        my.height = spec.height;
        my.width = spec.width;
        
        var me =  makeGeometricalFigure(spec, my);
        var draw = function () {
            log("makeSquare.draw"); 
            someDrawingLib.drawSquare(my.canvasDiv, 
                                      my.width,
                                      my.height,
                                      my.color);
        };
        var area = function () {
            return my.height * my.width;
        };
        me.draw = draw;
        me.area = area;
        return me;
    }

    function makeCube(spec, my) {
        log("makeCube");
        my = my || {};
        spec.height = spec.size;
        spec.width = spec.size;
        var me =  makeSquare(spec, my);
        return me;
    }

    function makeFilledCube(spec, my) {
        log("makeFilledCube");
        my = my || {};
        var me =  makeCube(spec, my);

        var fill = function () {
            someDrawingLib.fill(my.canvasDiv, my.color);
        };
        var drawCube = me.draw;
        me.draw = function () {
            log("makeFilledCube.draw");
            drawCube();
            fill();
        };
        return me;
    }

    function test() {
        log("test");
        var redCube = makeFilledCube({color: "red", size: 100});
        var blueCircle = makeCircle({color: "blue"});
        log("Objects created");
        var objects = [redCube, blueCircle];
        for (var i = 0 ; i < objects.length ; i = i + 1) {
            var figure = objects[i];
            log("Drawing object " + i);
            figure.draw(); // different methods are invoked depending on type
        }
        log("Demo finished");
    }

    function setupDemo() {
        // Add a button that starts test
        $('<button >Start demo</button>').appendTo('#start-button').click(test);    
    }
    setupDemo();
});