Object-Oriented Programming with jGrouse
- Subclassing - The jGrouse Way
- Other Hidden Sweets
- jGrouse Classes and jGrouse Modules
- Delegation vs. Multiple Inheritance
- Subclassing Magic Explained
- History of the question
Subclassing - The jGrouse Way
The function jgrouse.define(subclassName, superclassName, body)
is being used in jGrouse to define a subclass.
The simplest example of usage of that function is the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | jgrouse.define('my.SubClass', 'my.BaseClass',
function(embed)
{
eval(embed);
return {
initialize : function()
{
_sup(this, arguments);
this._newMember = 'abc';
},
someMethod : function()
{
_super(this, 'magicMethod', ['bla']);
}
}
}); |
In this example, the line #1 contains the names of the sublcass that is being defined and the name of its superclass.
The line #2 starts the definition of "class function", the function that would return the new members of the subclass.
The line #4 injects into hte scope of "class function" all the necessary statements that would provide ability to call methods from superclass, etc. See the details of that magic below
Developers can insert any code between lines #4 and #6. All the functions and variables that would be defined in that area would effectively become private static members of the subclass.
The line #6 starts the section where developers would actually define public methods of the subclass.
Note: the opening curly brace must be on the same line with the return
keyword.
The line #7 contains the definition of the constructor for the class. All constructors in jGrouse
framework have the name initialize. To be more precise, this is not constructor but rather
initializer, i.e. method that initializes member variables. As a matter of fact, jGrouse is using jgrouse.standardConstructor
as a standard constructor for all jGrouse classes, but developers never access that method directly.
Instead, they define the initialize method.
Note that it is necessary to call explicitly the initializer of superclass, the framework won't do it automatically. The reasons for that are described below.
The call _sup(this, arguments) on line #9 calls a method from superclass that has the same
name as the callee with parameters that were used for that call. In that case, this call is equivalent to the call
to my.BaseClass.prototype.initialize.apply(this, arguments), but much more convenient.
The line #15 shows an example of calling an inherited method that has name that is different from
callee's name. Essentially, it is like calling my.BaseClass.prototype.magicMethod.apply(this, ['bla']),
but again, in a shorter form.
Other Hidden Sweets
As it was mentioned above, the framework provides developers with ability to call the methods
from superclass via functions _sup and _super, but the framework provides
even more features out of the pocket. All the new members of the subclass would have access to the
following private static variables and functions:
function _sup(object, arguments)- call method of superclass that has the same name and accepts the same set of parametersfunction _super(object, methodName, arguments)- call method of superclass that either has different name from callee, or accepts different set of parameters.var _package- name of the parent javascript namespace. For example, if the name of superclass is 'com.foo.Bar', then the value of_packagewould be 'com.foo'var _class- reference to the true constructor (not initializer!) of the subclassvar _className- name of subclassl_log,l_error,l_warn,l_infoandl_debug- functions used to log messages. When used, these functions are passing to logger the information regarding the subclass, so the logging facility could provide more detailed information. The actual implementation of these functions depends on the value ofjgrouse_config.nologsconfiguration parameter - if it was specified, then all those functions would be generated as empty ones, thus suppressing logging.
In addition to that, the framework would attach to each member function an attribute __$memberName,
so each function would know under which name it appears in the subclass.
jGrouse Classes and jGrouse Modules
Although it is possible to use the definition of classes outside of jGrouse modules, defining them in modules have the following benefits:
- There is no need to worry that the file with superclass is included before the file with subclass. Just declare that subclass' module requires superclass' module and the framework would ensure the proper sequence of initialization
- Whenever several classes are defined in the same module, the framework would also "import" the classes in
the module into the context of each "class function". Thus if a module contains definition for two classes,
com.foo.Barandcom.foo.Boo, the members fromBoowould be able to refer tocom.foo.Barsimply asBar
Delegation vs. Multiple Inheritance
jGrouse does not directly support multiple inheritance. Such support has been considered, but it turned out that multiple inheritance brings into picture lots of ambiguities. As a result a decision was made that it was better to go "Java way" rather than "C++" way.
Essentially, in Java we have single inheritance with ability to specify that class implements a number of interfaces. In lots of cases the implementation of additional interfaces is delegated to a class member that already implements the required interface. Similar approach was taken with jGrouse
Java does not provide a simple way to implement delegation; the delegating methods must be created manually or using one of modern tools (like Eclipse). In jGrouse delegation is done with minimum lines of code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | jgrouse.define('my.SubClass', 'my.SuperClass', function(embed)
{
eval(embed);
var r = {
initialize : function()
{
_sup(this, arguments);
this._delegate = new SomeClass();
},
// more code to go
}
jgrouse.delegate(r, "_delegate", SomeClass.prototype);
return r;
})
|
Delegation is done by making a call to jgrouse.delegate(...) function at line 13.
The function would analyse what methods appear in SomeClass.prototype that does not
appear in r and would add specially crafted delegating methods to r.
The benefits of such approach are the following:
- It is quite clear when and how the delegate is being initialized. If delegate's constructor expects certain parameters, there is a definite spot where they could be supplied
- It becomes unambiguous to which data would the delegate methods be applied - they would apply to delegate itself, not to the class that is being defined.
- The chain of inheritance is clear and unambiguous
Subclassing Magic Explained
One might say, where are the definitions of all those functions _sup and _super? They
don't appear in jGrouse API and they are not a part of JavaScript?
As it was mentioned above, the "class function" should make a call to eval(embed) in order
to get benefits of all subclassing and other sweet things.
The magic is done by the framework, the way how the embed string is constructed. After the embed
string is evaluated, the code would look like this:
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 | jgrouse.define('my.SubClass', 'my.BaseClass',
function(embed)
{
var _package = 'my';
var _class = my.SubClass;
var _className = 'my.SubClass';
var _super = function(obj, method, args)
{
var arr = [];
for (var i = 2; i < arguments.length; i++)
{
arr.push(arguments[i])
};
return my.BaseClass.prototype[method].apply(obj, arr)
};
var _sup = function(obj, args)
{
var member = args.callee.__$memberName;
return my.BaseClass.prototype[member].apply(obj, args);
};
function l_log(level, msg) {jgrouse.log.log(level, msg, _className)};
function l_error(msg) {l_log(jgrouse.log.ERROR, msg)};
function l_warn(msg) {l_log(jgrouse.log.WARN, msg)};
function l_info(msg) {l_log(jgrouse.log.INFO, msg)};
function l_debug(msg){l_log(jgrouse.log.DEBUG, msg)}
return {
initialize : function()
{
_sup(this, arguments);
this._newMember = 'abc';
},
someMethod : function()
{
_super(this, 'magicMethod', ['bla']);
}
}
}); |
As you can see, the members that are being returned by "class function" are essentially closures, that are retaining access to local variables of the surrounding function
History of the question
JavaScript does not have concepts of classes and subclassing etc out of the pocket, at least not yet. JavaScript2 promises to address those concerns, but definitely it would take a while before all major browsers would start providing support for it and majority of the users would upgrade to these new versions of browsers.
Lots of people have made decent efforts to close that gap. Douglas Crockford has created a very good article regarding the theory of OOP for JavaScript. Since then lots of frameworks and libraries have been created, lots of those libraries are using concepts suggested by Douglas
Unfortunately the implementation of uber(...) from Cockford's article works reliably in a very limited cases. Earlier versions of Dojo toolkit (up to version 0.31) were using similar algorithm and the problems with it were highlighted by Alexander Netkachev in the bug that he submitted for Dojo. Dojo developers decided that the C++ approach is better and less ambiguous, so they have deprecated that feature for good in version 0.4. Of course it removed ambiguity, but I suppose that expressions like this one does not help readability and make refactoring more complex:
postMixInProperties: function(){
// some code
// now would call the method of superclass
dijit.layout.TabController.superclass.postMixInProperties.apply(this, arguments)
// some more code
}Dean Edwards has an interesting solution for this problem (see his article on that subject). In a nutshell, every method of subclass that overrides a method of superclass is replaced by a function that wraps around the subclass's method and has a reference to the superclass's method. From my perspective there are two problems with Dean's implementation of inheritance. One is relatively minor - it does not allow you to call any method from superclass, you're limited to calls to the method that is being overridden. But technically it is not a big deal to add such feature. The other problem is that every call of 'virtual' method becomes more expensive since wrapper code should be executed all the time.
As mentioned before, one of the aspects that has to be considered when designing a OO framework in JavaScript, is if the framework should be calling the constructors of superclass or if it should remain the responsibility of the developer. The Dojo developers have adopted the approach where Dojo would be calling all the constructors of superclasses. The downside of that is that if a subclass was expecting 5 parameters in its constructor, then the subclass's constructor should be dealing with the same set. That might be inconvenient when developer is creating a specialized subclass that would need to expose different set of parameters, assuming that superclass's part should be initialized with some kind of default values.
The other aspect that requires special consideration is whether the framework should support multiple inheritance. The developers of Dojo toolkit suggest that usage of mixins could be the answer in this case. But when mixins are being used, the following questions would require answers:
- Are mixins supposed to have member variables? If they are, how to handle the cases when the mixin have member variable(s) of the same name as the class that is being defined?
- If the mixin has a constructor and the constructor requires certain parameters, how and when it would be invoked?
- Is there a simple way to call inherited methods, other than specifying the full name of method that is being called?
Answers to such questions exist for C++, but hardly you would be able to find a solution for JavaScript that would be unambiguous and convenient to use. That's why jGrouse framework is using delegation instead of multiple inheritance.