DCL
A minimalistic yet complete JavaScript package for node.js
and modern browsers that implements OOP with mixins + AOP at both "class" and
object level. Implements C3 MRO
to support a Python-like multiple inheritance, efficient supercalls, chaining,
full set of advices, and provides some useful generic building blocks. The whole
package comes with an extensive test set (111 tests at the time of writing) and
fully compatible with the strict mode.
The package was written with debuggability of your code in mind. It comes with
a special debug module that explains mistakes, verifies created objects, and helps
to keep track of AOP advices. Because the package uses direct static calls to super
methods, you don't need to step over unnecessary stubs. In places where stubs are
unavoidable (chains or advices) they are small, and intuitive.
If you migrate your code from a legacy framework that implements dynamic (rather
than static) supercalls, take a look at the module "inherited" that dispatches
supercalls dynamically trading off the simplicity of the code for some run-time
CPU use, and a little bit less convenient debugging of such calls due to an extra
stub between your methods.
The main hub of everything dcl
-related is dcljs.org,
which hosts extensive documentation.
How to install
If you plan to use it in your node.js project install it
like this:
npm install dcl
For your browser-based projects I suggest to use volo.js:
volo install uhop/dcl
How to use
var dcl = require("dcl");
require(["dcl"], function(dcl){
});
define(["dcl"], function(dcl){
});
Inheritance, constructors, super calls
Let's continue with our coding example:
var Person = dcl(null, {
declaredClass: "Person",
name: "Anonymous",
constructor: function(name){
if(name){
this.name = name;
}
console.log("Person " + this.name + " is constructed");
}
});
var Bureaucrat = dcl(Person, {
declaredClass: "Bureaucrat",
constructor: function(name){
console.log("Bureaucrat " + this.name + " is constructed");
},
approve: function(document){
console.log("Rejected by " + this.name);
return false;
}
});
var clerk = new Bureaucrat();
clerk.approve(123);
As you can see it is trivial to define "classes" and derive them using
single inheritance. Constructors are automatically chained and called from
the farthest to the closest with the same arguments. Our Bureaucrat constructor
ignores name, because it knows that Person will take care of it.
Now let's do a mixin.
var Speaker = dcl(null, {
speak: function(msg){
console.log(this.name + ": " + msg);
}
});
var Talker = dcl([Person, Speaker], {
});
var alice = new Talker("Alice");
alice.speak("hello!");
Now let's call a method of our super class.
var Shouter = dcl(Speaker, {
speak: dcl.superCall(function(sup){
return function(msg){
if(sup){
sup.call(this, msg.toUpperCase());
}
};
})
});
var Sarge = dcl([Talker, Shouter], {
});
var bob = new Sarge("Bob");
bob.speak("give me twenty!");
The double function technique for a super call allows you to work directly with
a next method in chain --- no intermediaries means that this call is as fast as
it can be, no run-time penalties are involved during method calls, and it greatly
simplifies debugging.
And, of course, our "classes" can be absolutely anonymous, like in this one-off "class":
var loudBob = new (dcl([Talker, Shouter], {}))("Loud Bob");
loudBob.speak("Anybody home?");
AOP
We can use aspect-oriented advices to create our "classes":
var Sick = dcl(Person, {
speak: dcl.advise({
before: function(msg){
console.log(this.name + ": *hiccup* *hiccup* *hiccup*");
},
after: function(args, result){
console.log(this.name + ": *sniffle* I am so-o-o sick!");
}
})
});
var SickTalker = dcl([Talker, Sick], {
});
var clara = new SickTalker("Clara");
clara.speak("I want a glass of water!");
Hmm, both Talker
and Sick
require the same "class" Person
. How is it going
to work? Don't worry, all duplicates are going to be eliminated by the underlying
C3 MRO algorithm. Read all
about it in the documentation.
Of course we can use an "around" advice as well, and it will behave just like
a super call above. It will require the same double function technique to inject
a method from a super class.
var Martian = dcl(Speaker, {
speak: dcl.around(function(sup){
return function(msg){
if(sup){
sup.call(this, "beep-beep-beep");
}
};
})
});
var SickMartianSarge = dcl([Sarge, Sick, Martian], {
});
var don = new SickMartianSarge("Don");
don.speak("Doctor? Nurse? Anybody?");
For convenience, dcl
provides shortcuts for singular advices:
// pseudo code
dcl.before(f) == dcl.advise({before: f})
dcl.around(f) == dcl.advise({around: f})
dcl.after (f) == dcl.advise({after: f})
Chaining
While constructors are chained by default you can chain any methods you like.
Usually it works well for lifecycle methods, and event-like methods.
var BioOrganism = dcl(null, {
});
dcl.chainAfter(BioOrganism, "wakeUp");
dcl.chainBefore(BioOrganism, "sleep");
var SwitchOperator = dcl(null, {
wakeUp: function(){ console.log("turn on lights"); },
sleep: function(){ console.log("turn off lights"); }
});
var TeethBrusher = dcl(null, {
wakeUp: function(){ console.log("brush my teeth"); },
sleep: function(){ console.log("brush my teeth again"); }
});
var SmartDresser = dcl(null, {
wakeUp: function(){ console.log("dress up for work"); },
sleep: function(){ console.log("switch to pajamas"); }
});
var OfficeWorker = dcl([BioOrganism, SwitchOperator, TeethBrusher, SmartDresser], {
});
var ethel = OfficeWorker();
ethel.wakeUp();
ethel.sleep();
Advising objects
While class-level AOP is static, we can always advise any method dynamically,
and unadvise it at will:
var advise = require("dcl/advise");
var ethel = new (dcl(null, {
wakeUp: function(){ },
sleep: function(){ }
}))();
var wakeAd1 = advise(ethel, "wakeUp", {
before: function(){ console.log("turn on lights"); }
});
var wakeAd2 = advise(ethel, "wakeUp", {
before: function(){ console.log("brush my teeth"); }
});
var wakeAd2 = advise(ethel, "wakeUp", {
before: function(){ console.log("dress up for work"); }
});
var sleepAd1 = advise(ethel, "sleep", {
after: function(){ console.log("switch to pajamas"); }
});
var sleepAd2 = advise(ethel, "sleep", {
after: function(){ console.log("brush my teeth again"); }
});
var sleepAd3 = advise(ethel, "sleep", {
after: function(){ console.log("turn off lights"); }
});
ethel.wakeUp();
ethel.sleep();
wakeAd1.unadvise();
sleepAd1.unadvise();
wakeAd3.unadvise();
ethel.wakeUp();
ethel.sleep();
Again, for convenience, dcl/advise
provides shortcuts for singular advices:
// pseudo code
advise.before(obj, methodName, f) == advise(obj, methodName, {before: f})
advise.around(obj, methodName, f) == advise(obj, methodName, {around: f})
advise.after (obj, methodName, f) == advise(obj, methodName, {after: f})
Naturally "around" advices use the same double function technique to be super
light-weight.
Debugging helpers
There is a special module dcl/debug
that adds better error checking and reporting
for your "classes" and objects. All you need is to require it, and it will plug
right in:
var dclDebug = require("dcl/debug");
In order to use it to its fullest, we should include a static class id in our
"class" definitions like so:
var OurClass = dcl(null, {
declaredClass: "OurClass",
});
It is strongly suggested to specify declaredClass
for every declaration in every
real project.
This declaredClass
can be any unique string, but by convention it should be
a human-readable name of your "class", which possibly indicate where this class can
be found. For example, if you follow the convention "one class per file it can be
something like "myProject/aSubDir/aFileName"
. If you define several "classes"
per file you can use a following schema: "myProject/SubDirs/FileName/ClassName"
.
Remember that this name is for you, it will be reported in error messages and logs.
Yes, logs. The debug module can log constructors and objects created by
those constructors:
var A = dcl(null, {
declaredClass: "A",
sleep: dcl.after(function(){
console.log("*zzzzzzzzzzzzz*");
})
});
var B = dcl(A, {
declaredClass: "B",
sleep: function(){
console.log("Time to hit the pillow!");
}
});
var fred = new B();
advise.after(fred, "sleep", function(){
console.log("*ZzZzZzZzZzZzZ*")
});
fred.sleep();
dclDebug.log(A);
dclDebug.log(B);
dclDebug.log(fred);
This way we can always know that we generated correct classes, inspect static
chaining and advices, and even can monitor dynamically attached/removed advices.
Summary
Obviously this is a simple readme that was supposed to give an overview of dcl
.
For more details, please read the docs.
Additionally dcl
provides a small library of predefined
base classes,
mixins,
and useful advices. Check them out too.
Happy hacking!