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
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
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 ...
- complies to all criteria listed above
- uses neither "
this
" nor "new
" keywords - and, surprise: functional inheritance allows private instance variables and functions!
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
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:
Kommentar veröffentlichen