Blog-Archiv

Sonntag, 19. Oktober 2014

JS Functional Inheritance

We know that modern object-oriented programming languages provide inheritance to make life easier.
The lives of software developers that helplessly stray through jungles of inheritance hierarchies nobody found worth to document?
The lives of managers that can be sure that the maintenance of the software they sell will not make up 80% of the manufacturing costs, because it was written using OO techniques?

Inheritance lets us re-use existing, working and tested solutions, without any adapter code.
When using inheritance, we want to ...

  • benefit from the methods/fields of a super-class to solve new problems shortly and concise
  • overwrite methods the super-class implements, to change the class behaviour
  • call overwritten implementations from the sub-class, to re-use these defaults
  • implement an abstract super-class that solves problems in a general way, using abstract helper methods that have to be implemented then by concrete sub-classes
Generally spoken, inheritance is the best solution for the OnceAndOnlyOnce principle, which is one of the key criteria when it comes to the formidable software maintenance efforts.

So I want inheritance also in JavaScript. And I am confused about the many kinds of inheritance that seem to be available in JS, the most popular being

  • prototypal
  • pseudo-classical
  • functional
-> For me, one kind of inheritance that complies with criteria above would be enough!

The following is a kind of inheritance I developed during my JS studies. It is a slight extension of what is called "functional inheritance". It ...

  1. complies to all criteria listed above
  2. uses neither "this" nor "new" keywords
  3. and, surprise: functional inheritance allows private instance variables and functions!
I would call it "object-factory" inheritance.

Mind that it does not support the instanceof operator, I consider that being an anti-pattern.

Factory Functions

Base of this kind of inheritance are functions that create objects.
Here are basic factory functions for an "animal" inheritance tree:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var animal = function(species)   {
  var object = {}; // define the class

  object.species = species;
  object.belly = "empty";
 
  object.eat = function(food) {
    object.belly = food;
  };
  object.toString = function() {
    return "species '"+object.species+
      "', belly("+object.belly+
      "), sounding "+object.sound(); // calling abstract function
  };
 
  return object;
};

var mammal = function(species, name)   {
  var object = animal(species); // define the super-class

  object.name = name; // adding a property

  object.giveBirth = function(babyName) {
    return object.create(babyName); // calling abstract function
  };

  var superToString = object.toString; // prepare super-calls

  object.toString = function() { // override with super call
    return "I am '"+object.name+"', "+superToString();
  };
 
  return object;
};

Every hierarchy level is represented by exactly one function that creates an object representing that inheritance level.

The base function animal() first creates a new empty object, because this is the base of the inheritance hierarchy. It populates that object with some general properties and functions all animals should have.
Mind that there is a call to a function called sound() that does NOT exist in the object returned from animal(). I would call this an "abstract function", to be implemented by sub-objects of animal.

The function mammal() then "extends" animal() by calling it, copying the returned animal, and then enriching it with properties and functions of a mammal.
Copying is optional, only needed if a mammal wants to use overwritten super-functions or -properties. Each inheritance level would see only its direct ancestor, but as all properties/functions of the super-object are copied into the more specialized object, the final sub-object would see all properties/functions of all levels, except the overwritten ones.

A mammal also uses an "abstract function" create(), and it overwrites toString() and calls super.toString(), which is named superToString here (super is a JS reserved word).

Objects from Factories

Here comes the usual cat and dog stuff.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var cat = function(name) {
  var object = mammal("Cat", name); // define the super-class

  object.create = cat; // define constructor

  object.sound = function() { // implementation of abstract function
    return "Meaou";
  };
 
  return object;
};

var dog = function(name) {
  var object = mammal("Dog", name); // define the super-class

  object.create = dog; // define constructor
 
  object.sound = function() { // implementation of abstract function
    return "Wouff Wouff";
  };

  return object;
};

These two sub-objects both must implement the "abstract functions", which is sound() and create() in this case.
Mind that I can reference the function dog() within its own function body for the create() function!

For both factories I could have dropped the shallowCopy() call, because neither needs a super-call.
Also a create() function is necessary only when a Cat instance must be able to create another Cat instance.

Here is some test code ...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var garfield = cat("Garfield");
garfield.eat("catfish");
console.log(garfield.name+": "+garfield);

var catBaby = garfield.giveBirth("LittleGary");
console.log(garfield.name+"'s baby: "+catBaby);

var pluto = dog("Pluto");
pluto.eat("t-bone");
console.log(pluto.name+": "+pluto);

var dogBaby = pluto.giveBirth("LittleBloody");
console.log(pluto.name+"'s baby: "+dogBaby);

... and its output ...
Garfield: I am 'Garfield', species 'Cat', belly(catfish), sounding Meaou
Garfield's baby: I am 'LittleGary', species 'Cat', belly(empty), sounding Meaou
Pluto: I am 'Pluto', species 'Dog', belly(t-bone), sounding Wouff Wouff
Pluto's baby: I am 'LittleBloody', species 'Dog', belly(empty), sounding Wouff Wouff

Stepped into no gotcha, there are no bellies shared amongst these animals :-)

But, as I said before, the following prints false:

console.log("garfield instanceof cat: "+(garfield instanceof cat));

Reason is that there is no prototype used, and no new operator.
Here are some arguments against code using instanceof, which ...

  • speculates with the nature of classes, but that nature could change
  • is an excuse for not using correct sub-typing
  • implements things that should be implemented in the according class
Sorry for the term "class". We have no classes in JS, only objects. Although the factory functions above could be regarded as "class definitions".

Private Variables

Surprise: in functional inheritance private variables seem to work!
Try following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var vehicle = function(type) {
  var that = {};
    
  that.type = type;

  return that;
}

var motorbike = function(whose) {
  var that = vehicle('motorbike');

  var numberOfWheels = 2;

  that.whose = whose;
    
  that.wheels = function () {
    console.log(that.whose+' '+that.type+' has '+numberOfWheels+' wheels');
  };
  that.increaseWheels = function () {
    numberOfWheels++;
    console.log("Increasing wheels on "+that.whose+' '+that.type+" to "+numberOfWheels);
  };

  return that;
};

The factory function motorbike() hosts a local variable storing the number of wheels of the vehicle. It has an initial value, and a public function can increment that value. And as we see, each motorbike instance has its own wheel counter!

Following test code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var myMotorbike = motorbike("My");
myMotorbike.wheels(); 

var yourMotorbike = motorbike("Your");
yourMotorbike.wheels();

myMotorbike.increaseWheels();

myMotorbike.wheels();
yourMotorbike.wheels();

var hisMotorbike = motorbike("His");
hisMotorbike.wheels();

yourMotorbike.increaseWheels();
yourMotorbike.increaseWheels();

myMotorbike.wheels();
yourMotorbike.wheels();
hisMotorbike.wheels();

outputs this:

My motorbike has 2 wheels
Your motorbike has 2 wheels
Increasing wheels on My motorbike to 3
My motorbike has 3 wheels
Your motorbike has 2 wheels
His motorbike has 2 wheels
Increasing wheels on Your motorbike to 3
Increasing wheels on Your motorbike to 4
My motorbike has 3 wheels
Your motorbike has 4 wheels
His motorbike has 2 wheels

Lesson Learnt

To achieve module-encapsulation together with inheritance, JS functional inheritance provides all needed features.




Keine Kommentare: