dcl()
Version 2.x
dcl()
is the “class” composition helper, which simplifies creating constructors for new classes of objects:
- It reduces a boilerplate to set up a single inheritance.
- In case of mixin-based multiple inheritance it correctly linearizes all dependencies.
- While composing constructors, it can process advanced features:
- Super calls, when you need to call a method of a super class (a base).
- Define property descriptors inline (getters/setters, non-enumerable methods, and so on).
- AOP advices.
- Automatic method chaining.
Of course, an experienced programmer can do all this stuff manually, but dcl()
offers a less error-prone and more
compact way to achieve what you need. But, just in case, it will play nice with hand-made constructors, so your
constructors can inherit from them, or mix them in as mixins.
dcl()
is a main function of the whole package.
Description
1 2 3 4 5 6 7 8 9 10 |
|
Arguments and the return value:
base
- the base “class”. It can be one of three:null
- no base “class” ⇒ base it directly onObject
.- constructor function - function created with
dcl
or a generic JavaScript constructor function. New “class” will be based on it using a single inheritance. - array of constructors - C3 superclass linearization will be performed on this array, and the result will be used to create a new constructor using a single inheritance. This array should be non-empty.
props
- the object, whose properties are used to specify unique features of the “class” — methods, and class-level variables.props
can be an instance of dcl.Prop. In this case, it specifies property descriptors directly, like for Object.defineProperties() or Object.create(). Following properties have a special meaning fordcl()
:constructor
- the optional function, which will be called when an object is created. A base constructor (if any) is always called before a derived constructor. There is no restrictions on what arguments can be used for a constructor. A return value of constructor (if any) is ignored.declaredClass
- the optional human-readable string, which is used to identify the created class in error messages or logs. See debug.js for more details.
options
- the optional object, which specifies defaults for a descriptor of Object.defineProperty().- Following property names are recognized:
enumerable
,configurable
,writable
,detectProps
. - If
detectProps
istrue
:- All regular properties are checked to be an object.
- If that object has only a valid subset of following properties:
get
,set
,value
,enumerable
,configurable
,writable
— it is considered to be a property descriptor, which has the same effect as usingdcl.prop()
to wrap it.
- If that object has only a valid subset of following properties:
- More info on property descriptors can be found in Object.defineProperty().
- All regular properties are checked to be an object.
- If
enumerable
,configurable
, orwritable
are Boolean values, they are added verbatim to all descriptors, which do not define those properties explicitly. It applies to property descriptors generated from regular properties bydcl()
. enumerable
,configurable
, orwritable
can be a procedure with two arguments:descriptor
is a property descriptor object, which can be inspected and augmented/corrected, if needed.name
is a property name as a string.
- Following property names are recognized:
dcl()
returns the constructor created according to user’ specifications.
dcl()
hosts additional properties. Look at specific modules to learn what public properties are available.
Examples
No base
In this case produced “classes” are derived directly from Object
.
1 2 3 4 5 6 7 8 9 10 |
|
It will be the same as this:
1 2 3 4 5 6 7 8 9 10 |
|
Single inheritance
This is a familiar scenario for most programmers: we build on an existing “class” producing a new “class”.
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 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
We can base our classes on normal JavaScript constructors as well:
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 |
|
Multiple inheritance with mixins
The dreaded diamond — the bane of multiple inheritance.
dcl()
deals with it with ease:
1 2 3 4 5 |
|
A triangle will be handled too:
1 2 3 4 |
|
Generally a “class”, which inherits from an array, tries to use the first item as its base. In some cases it is not
possible due constraints on relative position of all bases. If that is the case, Object
is used.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
If F
can be based on D
, our right-most list of dependencies should look like this: D, B, A
, which is not the case.
dcl()
cannot preserve this list because C
should go before B
as defined in E
. It forces dcl()
to base F
directly on Object
.
For available debugging facilities take a look at log() method defined by debug.js.
If you want to test, whether or not an object is inherited directly (in JavaScript sense) or indirectly (e.g., as a mixin), consider using isInstanceOf().
Make all properties read-only
1 2 3 4 5 6 |
|
Make specific property read-only
1 2 3 4 5 6 7 8 9 10 |
|
Define a property using a descriptor
For that, we will need dcl.prop().
1 2 3 4 5 6 7 8 9 10 11 |
|
Detect a property descriptor automatically
This style is useful, when mixing seamlessly regular (pre-ES5) properties with fancy property descriptions.
No dcl.prop()
is required.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Define a “class” using descriptors
All properties can be described as an object that contains property descriptors.
1 2 3 4 5 6 7 |
|
Notes
- Constructors are chained using “after” chaining, meaning that a derived constructor will be called only after its base constructor.
- Missing constructors are treated as empty constructors.
- All constructors are called with the same set of arguments.
- If a constructor returns a value, it will be ignored.
- All unchained methods (the default), and other properties, override properties with the same name in base classes.
- Always use
new
keyword when creating objects with a constructor produced bydcl()
. dcl()
reuses property descriptors augmenting them, if needed.
FAQ
How can I detect, if my class inherits directly or indirectly from A
?
You can always use isInstanceOf():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
How does dcl()
linearize superclasses?
dcl()
uses the C3 superclass linearization algorithm. This is the
same proven algorithm that powers Python (since 2.3), Perl 6, Parrot, and Dylan. The best discussion of this algorithm
I know of is in this paper: The Python 2.3 Method Resolution Order.
Effectively this algorithm is a topological sorting of an inheritance graph with complexity O(n).
Why do I need the linearization? I don’t do no “diamonds” in my code.
Unfortunately dependency loops, and reversed dependencies can happen accidentally, especially in big projects, which are
targeted by this OOP package. It is very difficult to keep track of all mixins, and their dependencies, especially when
they are written by other programmers, or even 3rd-party shops. dcl()
keeps track of all these matters for you
linearizing bases, eliminating duplicates, and checking the overall consistency of your “classes”.
Many JS OOP packages skip the correct linearization step hoping that everything will be right as is. Unfortunately it can be a source of extremely hard-to-find bugs. Besides mixins use inheritance routinely to indicate their relative dependencies so duplicates will occur naturally.
I write only small programs. I don’t need no linearization, right?
If you write only small programs, chances are you don’t need OOP. See discussion of the OOP applicability area in OOP and JS, specifically “fail #2”.
Can I use hand-made constructors as bases or mixins with dcl()
?
Yes, you can. It was demonstrated in the code example above. Obviously your hand-made classes cannot use dcl()
-based
facilities like super calls, or class-level advices, but other than that they can be used without restrictions.
Is it possible to chain methods other than constructor?
Yes. See chainBefore() and chainAfter() directives.
Is it possible to use advices with constructors?
Yes. The full set of advices can be used with constructors.
Is it possible to “break” a chain of constructors or other chained methods?
While it is not advised due to possible violation of object invariants, and potential maintenance problems, you can do it with super calls — just define a super call and doesn’t call a super.
See superCall() for more details.
See discussion of object invariants in OOP and JS and in OOP in JS revisited.
Can I use []
instead of null
as my base?
Yes, but it is not recommended.
In any case null
is a constant, which is shared, while []
is a newly-created object. The latter comes with
a penalty (it has to be created, and it will add a load to the garbage collector afterwards. null
is cheaper, and
clearly demonstrates programmer’s intent.
If you do not calculate bases, it is easier to skip them completely, rather than use null
.
Can I use [base]
instead of base
as my base?
Yes, but why? The former will create an additional array object, which will be discarded right after the dcl()
call
increasing the load on the garbage collector. The latter is clearly cheaper, and more intentional.