Classes vs. Objects
We’ve just seen various theoretical explorations and mental models of “classes” vs. “behavior delegation”. But, let’s now look at more concrete code scenarios to show how’d you actually use these ideas.
We’ll first examine a typical scenario in front-end web dev: creating UI widgets (buttons, drop-downs, etc).
Widget “Classes”
Because you’re probably still so used to the OO design pattern, you’ll likely immediately think of this problem domain in terms of a parent class (perhaps called Widget
) with all the common base widget behavior, and then child derived classes for specific widget types (like Button
).
Note: We’re going to use jQuery here for DOM and CSS manipulation, only because it’s a detail we don’t really care about for the purposes of our current discussion. None of this code cares which JS framework (jQuery, Dojo, YUI, etc), if any, you might solve such mundane tasks with.
Let’s examine how we’d implement the “class” design in classic-style pure JS without any “class” helper library or syntax:
// Parent class
function Widget(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
Widget.prototype.render = function($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
};
// Child class
function Button(width,height,label) {
// "super" constructor call
Widget.call( this, width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
}
// make `Button` "inherit" from `Widget`
Button.prototype = Object.create( Widget.prototype );
// override base "inherited" `render(..)`
Button.prototype.render = function($where) {
// "super" call
Widget.prototype.render.call( this, $where );
this.$elem.click( this.onClick.bind( this ) );
};
Button.prototype.onClick = function(evt) {
console.log( "Button '" + this.label + "' clicked!" );
};
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = new Button( 125, 30, "Hello" );
var btn2 = new Button( 150, 40, "World" );
btn1.render( $body );
btn2.render( $body );
} );
OO design patterns tell us to declare a base render(..)
in the parent class, then override it in our child class, but not to replace it per se, rather to augment the base functionality with button-specific behavior.
Notice the ugliness of explicit pseudo-polymorphism (see Chapter 4) with Widget.call
and Widget.prototype.render.call
references for faking “super” calls from the child “class” methods back up to the “parent” class base methods. Yuck.
ES6 class
sugar
We cover ES6 class
syntax sugar in detail in Appendix A, but let’s briefly demonstrate how we’d implement the same code using class
:
class Widget {
constructor(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
}
class Button extends Widget {
constructor(width,height,label) {
super( width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
}
render($where) {
super.render( $where );
this.$elem.click( this.onClick.bind( this ) );
}
onClick(evt) {
console.log( "Button '" + this.label + "' clicked!" );
}
}
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = new Button( 125, 30, "Hello" );
var btn2 = new Button( 150, 40, "World" );
btn1.render( $body );
btn2.render( $body );
} );
Undoubtedly, a number of the syntax uglies of the previous classical approach have been smoothed over with ES6’s class
. The presence of a super(..)
in particular seems quite nice (though when you dig into it, it’s not all roses!).
Despite syntactic improvements, these are not real classes, as they still operate on top of the [[Prototype]]
mechanism. They suffer from all the same mental-model mismatches we explored in Chapters 4, 5 and thus far in this chapter. Appendix A will expound on the ES6 class
syntax and its implications in detail. We’ll see why solving syntax hiccups doesn’t substantially solve our class confusions in JS, though it makes a valiant effort masquerading as a solution!
Whether you use the classic prototypal syntax or the new ES6 sugar, you’ve still made a choice to model the problem domain (UI widgets) with “classes”. And as the previous few chapters try to demonstrate, this choice in JavaScript is opting you into extra headaches and mental tax.
Delegating Widget Objects
Here’s our simpler Widget
/ Button
example, using OLOO style delegation:
var Widget = {
init: function(width,height){
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
},
insert: function($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
};
var Button = Object.create( Widget );
Button.setup = function(width,height,label){
// delegated call
this.init( width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
};
Button.build = function($where) {
// delegated call
this.insert( $where );
this.$elem.click( this.onClick.bind( this ) );
};
Button.onClick = function(evt) {
console.log( "Button '" + this.label + "' clicked!" );
};
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = Object.create( Button );
btn1.setup( 125, 30, "Hello" );
var btn2 = Object.create( Button );
btn2.setup( 150, 40, "World" );
btn1.build( $body );
btn2.build( $body );
} );
With this OLOO-style approach, we don’t think of Widget
as a parent and Button
as a child. Rather, Widget
is just an object and is sort of a utility collection that any specific type of widget might want to delegate to, and Button
is also just a stand-alone object (with a delegation link to Widget
, of course!).
From a design pattern perspective, we didn’t share the same method name render(..)
in both objects, the way classes suggest, but instead we chose different names (insert(..)
and build(..)
) that were more descriptive of what task each does specifically. The initialization methods are called init(..)
and setup(..)
, respectively, for the same reasons.
Not only does this delegation design pattern suggest different and more descriptive names (rather than shared and more generic names), but doing so with OLOO happens to avoid the ugliness of the explicit pseudo-polymorphic calls (Widget.call
and Widget.prototype.render.call
), as you can see by the simple, relative, delegated calls to this.init(..)
and this.insert(..)
.
Syntactically, we also don’t have any constructors, .prototype
or new
present, as they are, in fact, just unnecessary cruft.
Now, if you’re paying close attention, you may notice that what was previously just one call (var btn1 = new Button(..)
) is now two calls (var btn1 = Object.create(Button)
and btn1.setup(..)
). Initially this may seem like a drawback (more code).
However, even this is something that’s a pro of OLOO style code as compared to classical prototype style code. How?
With class constructors, you are “forced” (not really, but strongly suggested) to do both construction and initialization in the same step. However, there are many cases where being able to do these two steps separately (as you do with OLOO!) is more flexible.
For example, let’s say you create all your instances in a pool at the beginning of your program, but you wait to initialize them with specific setup until they are pulled from the pool and used. We showed the two calls happening right next to each other, but of course they can happen at very different times and in very different parts of our code, as needed.
OLOO supports better the principle of separation of concerns, where creation and initialization are not necessarily conflated into the same operation.