I worked in large RIA projects with tons of JavaScript code, and I also worked in common request/response-based projects with rich components where also was a lot of JavaScript code. In both cases I took part in infrastructure decisions and I remember myself being of the "let's use prototyped classes to structure our code" guys. This is my mea culpa :)
After a while I learnt that inheritance in JavaScript isn't like in Java. And the common mistake is to think an object model in JavaScript like if we were coding Java (or another strongly-typed object-oriented language). Don't blame yourself, I know you found a lot of JS libraries that tell you that it is good, they bury into your mind that you've got to build classes and they have to look like in Java. So we find things like this:
MyClass = Class.create({
_message: null,
initialize: function (message) {
console.log("Hi!, I'm a constructor");
this._message = message;
},
talk: function () {
alert(this._message);
}
});
I didn't write comments in the code because I want to show how odd and weird it is without any noise.
First of all, note that a "private" attribute is prefixed with underscore (_message). It is what I call "the private trap". It implies:
- We only trust in a weak convention to indicate that an attribute is private, but we're exposing it anyway.
- At the end of the day, when you're enough broke to respect conventions and you need to deliver a product next morning, you will not tremble to change "private" values, which can be a catastrophic mistake since it can be shadowing design limitations.
- The most important: you cannot think in terms of software design.
The second consideration is that there exist a "virtual constructor". This is often defined as a convention by libraries since it's not possible to implement transparent prototype inheritance with native constructors. This approach has the following issues:
- It's not standard. Usually it changes from library to library, so you cannot simply reuse code without adding required libraries, which sometimes aren't compatible between them.
- It's not clear for people that don't know the library you're using. There're some considerations regarding objects lifecycle that need to be well-understood before start working on a class.
- Conventions proliferate everywhere. Conventions are out there to help us understand between us, if they make our work harder, they should be removed.
I'm not saying that you cannot design well by using this pattern, I'm trying to tell that this pattern actually induces you like a devil to stop thinking and start coding before you understand what you need to solve. The threshold to do not stop and think is very low.
This pattern also aborts several good practices and design principles. For example, you cannot design thinking in "providers and consumers". You cannot document contracts because all the public interface is full of useless fields.
You need to bind every method to the object (sometimes more than once) to make the instance available in callbacks. Usually the DOM Event Model is broken because the scope in callbacks is not "the object receiving the event" as the spec says, but the object instance. It's not just about keeping standards, it also may cause memory leaks impossible to track, or it may cause annoying bugs like event handlers registered more than once.
And of course, all user-related interaction methods like event handlers are mixed into the object together with the business-specific logic. You cannot make your model agnostic (whatever it means).
I understood some of these concerns implementing classes this way, however I told to myself: this is the truth about make your application to scale up to the sky. And it was a terrific lie. Your application won't scale by using this pattern. You generate a strong coupled set of classes that cannot survive alone. You easily fall into the yo-yo antipattern, and it can be an unrecoverable mistake in JavaScript. It often results in an anemic model that adds accidental complexity to your codebase, making very harder to refactor what already exist.
I like to compare this pattern with writing in stone.
More or less that was the scenario when we decided to change our minds and find a solution to these problems. After several discussions and research, we agreed with the following pattern:
MyClass = function (message) {
console.log("Hi!, I'm a constructor");
return {
talk: function () {
alert(message);
}
};
};
Yes, this is the well-known module pattern. Look at the code, it's cleaner, doesn't it?. Let's say something about it:
- You don't need to store constructor parameters in private variables.
- You can expose only public, defined-by-contract methods.
- You can simplify interfaces and think about what's public and what is not before start coding the implementation.
- You can easily refactor this code, because the public interface is what consumers see and it's implementation-agnostic. It means that you can safely rewrite the full implementation only respecting the documented contract.
- There's no more no function binding. I have been implementing this pattern for some months and there's no one case in which I had to bind a function to an object (yes, maybe you think I'm lying, it sounds like the paradise!).
- Augmentation (inherit methods and fields)
- Methods override.
- Invoke superclass methods.
Subclass = function (message) {
var base = new MyClass(message);
var talk = base.talk;
return jQuery.extend(base, {
feed: function () {
this.talk("mmm!");
},
talk: function (whisper) {
console.log("Psst: " + whisper);
talk(message);
}
});
};
Note that the implementation is composing the base class MyClass instead of inheriting from it, and the constructor returns a mixin (I'm using jQuery.extend by convenience, but you can implement it yourself) that merges the base object and the new behaviour. It's more like composing behaviour instead of inheriting from classes (and of course, you can compose more than one object into a mixin).
The method-override is also pretty simple. It stores the original method in a private variable, and the mixin just overrides the method and it calls the original from inside. A similar strategy is used to access superclass methods: you can invoke them accessing the base variable instead of using this. That is another level of safety to avoid function binding and it also gives you the idea of composition instead of inheritance.
Opposite to the Writing in Stone pattern, I call it Drawing in the Sand.
Next time you have to structure UI applications think about this. Do I want to write in the stone, or do I prefer to draw in the sand? If you successfully answer this question, you'll be around the KISS principle, that is essential in a mutable-hostile environment like it is a web application.-