Encapsulation in object-oriented programming languages always was a much disputed topic:
- Doesn't encapsulation prevent code re-usage?
- Why is it necessary to hide functionality?
- Wouldn't this be a wonderful world if everything was public?
From my point of view the answer is
No, it would be an unforgiving and unpredictable world, where the wing beat of a butterfly on one side could cause a hurricane on the other side.It's all about complexity, and how to border it. Complexity is like the water of a river after ten days of rain - it sweeps away everything.
Imagine many small problem solutions, forming the solution of a bigger
problem, which again is just one of many solutions for an even bigger problem.
Now the solution for the top problem doesn't work any more, and you must find the error.
Imagine everything in the source code is public. The error could be everywhere, because the
programmers could have called everything from everywhere. But when they have
separated public from private methods, the possible hazards are significantly less.
So what you need to do as software developer is
- solve a problem
- put the solution into a capsule, leaving open just the necessary access points (hiding complexity increases usability)
- use it, together with other such capsules, to build the solution for a bigger problem
Remember:
Hide as much complexity as possible,
don't confront the outside world with complex details.
When JavaScript programmers talk about modules, they mostly mean objects
with properties and functions that have access to nested variables and
functions, the latter not being accessible from outside.
But JS does not have access modifiers. How can we achieve encapsulation?
Encapsulation Archetype
When you want to write some JS code without polluting the global namespace with variables and functions, you can do the following:
(function() { console.log("I am executing!"); }());
This is called a self-executing function. This kind of suicide is caused by the
terminating parentheses (:-).
The comprehensive parentheses around the whole function definition are needed
to be syntactically correct.
But it doesn't seem to matter if they are just around the function
definition or around all code, the following also works:
(function() { console.log("I am also executing!"); })();
You can also parameterize this capsule:
(function(what) { console.log("I am "+what+"!"); }("doing whatever you want"));
This outputs:
I am doing whatever you want!
Whatever you write inside the self-executing function will not "pollute" the global namespace. And it will be executed as soon as the JS interpreter encounters the terminating parentheses. So this represents an encapsulation archetype.
Module Archetype
Lets go JS-extreme and try a private function and variable:
var printer = (function() { var defaultWhat = "Default Hello World"; function print(what) { console.log(what || defaultWhat); } return { print: print }; })(); printer.print(); printer.print("Hello World");
When called, this outputs:
Default Hello World Hello World
This is called the "Revealing Module Pattern" , because it solves problems in a private hidden space, and finally exports some public access points in the returned object.
But wait, here again we "polluted" the global namespace with a variable printer
,
holding a singleton object. Can't we do better?
Namespace Archetype
We can hide all our upcoming variables into our own namespace. However, we always must reserve a name for our namespace when it needs to be globally available.
A JS-namespace is an object, possibly holding other namespace objects. This might lead to a hierarchy of namespaces, built using the following pattern:
var my = my || {}; my.module = my.module || {}; my.module.printer = my.module.printer || { print: function(what) { console.log(what); } }; my.module.printer.print("Hi there");
Basically this reads as
"let variable 'my' be either the already existing 'my', or a new object when not yet defined."So the object is created only the first time the JS interpreter executes this line.
We also could have written this like the following (with an ugly amount of indentation):
var my = my || { module: { printer: { print: function(what) { console.log(what); } } } }; my.module.printer.print("Hi there");
But mind that this is not exactly the same. When my
already existed at this point,
the module
and printer
objects
would not have been written into the namespace! So better do it in the way shown before.
We could now drop properties/functions in the my
space, or in the my.module
space. Mind that everything in an object is visible
and mutable from outside (public). The namespace is not a capsule, it is just a
convenience, using long dotted names instead of simple names that once could
get ambiguous.
Namespaced Module Instances
How about a function that dynamically creates new printer instances which can have a private state?
var my = my || {}; my.module = my.module || {}; my.module.createPrinter = my.module.createPrinter || function(toPrint) { var running = false; var on = function() { running = true; console.log("Ra ta ta .... "); }; var off = function() { console.log("... ta ta: "+toPrint); running = false; }; var state = function() { return running; }; return { start: on, stop: off, isRunning: state }; };
There is a private state in any created printer, represented by the
running
variable, and by the toPrint
parameter.
The latter is captured by the function closure and thus is available
inside the function like a variable.
Mind that you shouldn't implement any function in the return-object. Just return references to the private functions inside the module capsule.
Here is some test code for this printer factory function:
var myFirstPrinter = my.module.createPrinter("First Hello World!"); console.log("myFirstPrinter.isRunning(): "+myFirstPrinter.isRunning()); myFirstPrinter.start(); console.log("myFirstPrinter.isRunning(): "+myFirstPrinter.isRunning()); var mySecondPrinter = my.module.createPrinter("Second Hello World!"); console.log("mySecondPrinter.isRunning(): "+mySecondPrinter.isRunning()); mySecondPrinter.start(); console.log("mySecondPrinter.isRunning(): "+mySecondPrinter.isRunning()); myFirstPrinter.stop(); console.log("myFirstPrinter.isRunning(): "+myFirstPrinter.isRunning()); console.log("mySecondPrinter.isRunning(): "+mySecondPrinter.isRunning()); mySecondPrinter.stop(); console.log("myFirstPrinter.isRunning(): "+myFirstPrinter.isRunning()); console.log("mySecondPrinter.isRunning(): "+mySecondPrinter.isRunning());
This outputs:
myFirstPrinter.isRunning(): false Ra ta ta .... myFirstPrinter.isRunning(): true mySecondPrinter.isRunning(): false Ra ta ta .... mySecondPrinter.isRunning(): true ... ta ta: First Hello World! myFirstPrinter.isRunning(): false mySecondPrinter.isRunning(): true ... ta ta: Second Hello World! myFirstPrinter.isRunning(): false mySecondPrinter.isRunning(): false
As we see each printer has its own private state, not intervening with the states of other printer instances.
Module Instantiation with Inheritance
We can create module singletons, we can instantiate modules, now we want to instantiate modules that inherit from other modules!
I strongly advice to use
functional inheritance,
it is simple and transparent.
What we always need for functional inheritance is a function that can
copy a super-object, to let reuse the functionality implemented there.
Here is a simple utility to do that:
var shallowCopy = function(source) { if (source instanceof Function || ! (source instanceof Object)) throw "shallowCopy() expected Object but got "+source; var target = {}; var propertyName; for (propertyName in source) if (source.hasOwnProperty(propertyName)) target[propertyName] = source[propertyName]; return target; }
When you have jQuery in your web page, you can use jQuery.extend() instead of this. jQuery is for browser engines what Java is for operating systems, so use it wherever you can.
We can derive the printer
instance to be a suspendable printer
:
my.module.createSuspendablePrinter = my.module.createSuspendablePrinter || function(toPrint) { var printer = my.module.createPrinter(toPrint); // define super-object var base = shallowCopy(printer); // save old implementations var suspended = false; printer.suspend = function() { suspended = true; console.log("... suspending ..."); }; printer.resume = function() { console.log("... resuming ..."); suspended = false; }; printer.isRunning = function() { // override if (base.isRunning() && suspended) // call super return false; return base.isRunning(); }; return printer; };
These test lines
var mySuspendablePrinter = my.module.createSuspendablePrinter("Hello Suspendable World!"); mySuspendablePrinter.start(); console.log("Started mySuspendablePrinter.isRunning(): "+mySuspendablePrinter.isRunning()); mySuspendablePrinter.suspend(); console.log("Suspended mySuspendablePrinter.isRunning(): "+mySuspendablePrinter.isRunning()); mySuspendablePrinter.resume(); console.log("Resumed mySuspendablePrinter.isRunning(): "+mySuspendablePrinter.isRunning()); mySuspendablePrinter.stop(); console.log("Stopped mySuspendablePrinter.isRunning(): "+mySuspendablePrinter.isRunning());
output the following
Ra ta ta .... Started mySuspendablePrinter.isRunning(): true ... suspending ... Suspended mySuspendablePrinter.isRunning(): false ... resuming ... Resumed mySuspendablePrinter.isRunning(): true ... ta ta: Hello Suspendable World! Stopped mySuspendablePrinter.isRunning(): false
Hm, this printer is more a noise machine than something useful ... one more bad example :-(
Keine Kommentare:
Kommentar veröffentlichen