In my
Blog about Encapsulation and Inheritance
I listed the characteristics of an object-oriented language.
In my
Blog about the JS Module Pattern
I tried to explain why encapsulation is necessary to hide complexity.
In this Blog I will try to bring the
Revealing Module Pattern
into a reusable shape, and explain why
singletons
are not enough to achieve a good JavaScript code base.
Requirements
What would we like to achieve?
- Encapsulation: having (at least) private and public fields and functions,
private functions can access public functions and fields,
public functions can access private functions and fields
- Inheritance: a public function can be overwritten to implement a slightly different behaviour,
and private functions calling it must then automatically meet the override
- Calling super: a sub-object can use an overwritten implementation of its super-object
- Abstract functions: a super-object can call functions that are implemented in
the sub-object object only
Example Code
The Revealing Module Pattern is called "revealing" because it exposes just its public parts through the return-object.
In text-area below you can try out the example code by pressing the "Run" button,
and you can also modify it.
I call the function animal
a "module" because it contains nested functions,
and it returns an object that has been built inside.
I do not consider "module" to be an adequate term for that, but I comply to the JS tradition.
As "module", in this context, we could understand something that is more than a function or object (map).
We can't call it "class" because JS has no type-concept.
Source Code Explanations
Every local variable inside animal
, like belly
or species
,
will survive the function call when it is referenced by the returned object in some way (and thus is in its closure).
We could call such variables private instance-fields.
The same for inner functions. None of the private variables and functions,
like belly
or getBelly()
, is visible or accessible from outside.
On the other hand, any function and variable in the return-object is public.
They could be overwritten by any callee, simply assigning another function to the function's name.
But mind that these overrides do not have access to the private parts of the module.
Also the private functions can not call the public ones, because these are just in the return object on bottom,
and that object does not have a name that could be referenced.
Code inside Return
Now you could say "I can do this much shorter":
var animal = function(givenSpecies)
{
var species = givenSpecies;
var belly = "nothing";
return {
hasEaten: function() {
return belly;
},
eat: function(food) {
belly = food;
},
toString: function() {
return species+" has eaten "+belly;
}
};
};
Putting code into the returned functions is not a good practice.
(1) First drawback is that private functions can not call the public ones,
because these are in an anonymous return object.
(2) Second drawback is that functions inside the return-object can call each other only by specifying this
, it would not work without:
var animal = function(givenSpecies)
{
....
return {
hasEaten: function() {
return belly;
},
....
,
toString: function() {
return species+" has eaten "+this.hasEaten();
}
};
};
(3) Third drawback is that you increase the nesting level.
The deeper the nesting level, the more the problem of global variables comes into sight
(nobody knows why they have a wrong value at the right time:-).
The more lines of code see a variable, the more side-effects will take place, the more unstable everything will be.
Holding variable values is the primary reason for encapsulation,
but having deeply nested inner logic below is counterproductive.
var animal = function(givenSpecies)
{
var belly = "nothing";
....
return {
sound: function() {
return {
loud: function() {
// belly is read/writable here
},
quiet: function() {
// belly is read/writable here
}
};
},
....
};
};
Mind that a variable like belly
is visible and writable
anywhere inside the animal()
function, even in the deepest nested object or function.
Always try to keep the nesting level of your JS code as low as possible.
It will get unreadable else, and variables in upper scopes will get uncontrollable.
Singletons prevent Inheritance
Mostly the Revealing Module Pattern is shown like this:
var animal = (function(givenSpecies)
{
....
return {
....
};
})();
The difference to the pattern above is that animal
now is an object-instance.
You can not call it any more as function to produce a new instance.
It is an IIFE.
The resulting animal
object is called singleton.
Such is also used in object-oriented languages, but just in special cases.
A singleton reduces the functionality to just one instance.
If you replace functions or properties on it, the singleton instance itself would be changed.
The only way to use a JS singleton for inheritance is copying it, but then you may copy a particular state of the singleton.
Instantiation modules, or let's call them factory functions, are much more useful than singletons.
In the following code, animal
is extended to be a named cat
.
Try out this example, it shows that different instances can have different food
in their private belly
.
var animal = function(givenSpecies)
{
var species = givenSpecies;
var belly = "nothing";
var getBelly = function() {
return belly;
};
var setBelly = function(food) {
belly = food;
};
var toString = function() {
return species+" has eaten "+getBelly();
};
return {
hasEaten: getBelly,
eat: setBelly,
toString: toString
};
};
var cat = function(givenName)
{
var name = givenName;
var self = animal("Cat");
var superToString = self.toString;
self.toString = function() {
return name+" "+superToString();
};
return self;
};
var garfield = cat("Garfield");
garfield.eat("mouse");
alert(garfield.toString());
var catbert = cat("Catbert");
catbert.eat("rat");
alert(catbert.toString());
In this example, a module cat
extends animal
.
It instantiates animal
, and then replaces its toString()
function.
To be able to reuse the super-implementation of toString()
, it stores the
old function-pointer into a local variable superToString
before replacing it.
Do not make it difficult to reuse source code in JS modules by implementing them as singletons.
But mind that a singleton would make sense when you have no instance variables,
because you would not need to instantiate it before using it, and that makes life simpler.
You could call this a "data-less module".
Naming the Return
The module cat
looks different than animal
.
Here the return-object has a name, and that name could be referenced also by private functions.
Hence the named-return form would be preferable.
So why not doing also the base object with a named return object?
In the following the return-object is named self
,
but you can give it any name except
JS reserved words like this
.
Some call it that
, another good name would be instance
.
var animal = function(givenSpecies)
{
var species = givenSpecies;
var belly = "nothing";
var self = {};
var getBelly = function() {
return belly;
};
var setBelly = function(food) {
belly = food;
};
var sound = function() {
if (self.hasEaten() !== "nothing") // call public from private
return "hummm ...";
return "grumble ...";
};
self.hasEaten = getBelly;
self.eat = setBelly;
self.sound = sound;
self.toString = function() {
return species+" has eaten "+getBelly(); // call private from public
};
return self;
};
var dog = animal("Dog");
alert(dog.toString()+", sounds: "+dog.sound());
dog.eat("sausage");
alert(dog.toString()+", sounds: "+dog.sound());
Yes, you are right, why have both private and public functions doing the same?
This is just an example how public functions could call privates, and vice versa.
In a real world you would do it shorter.
Mind that even if you override animal
now and overwrite the hasEaten()
function,
the sound()
implementation would work correctly by calling the replaced hasEaten()
,
not the original one.
Naming the return definitely is an advantage.
Which now has led the Revealing Module Pattern to be what is called
functional inheritance.
Now let's check if we have met our start-requirements.
There is everything except the abstract function.
But this is really easy: just call the function in super-object,
and make sure that it is implemented then in the module that extends the super-object.
The JS interpreter will search for the function only when it actually is launched, not before.
In a strongly typed language like Java this would not be possible,
but JavaScript is a rubber language :-)