I was recently re-factoring some code under Rick‘s guidance, and we implemented what I would later recognize as the Strategy pattern. JavaScript’s objects and first-class functions make this pattern extremely simple to implement, although you can optionally take some additional steps to add robustness.
Background: Design Patterns
A lot of my troubles with design patterns come from understanding their goal and recognizing appropriate situations to use them. In fact, many people begin practicing them without any formal education into design patterns. This leads many seasoned programmers to respond with, “well, duh” when first confronted. Design patterns are derived from scrutiny of best practices in the real world (not your old CS prof’s black cauldron). They can seem artificial because they have been abstracted to describe general programming paradigms. That means any discussion of a specific pattern should really begin with an explanation of use cases–keep reading!
Motivation: Why Strategy?
Abstractly speaking, the Strategy pattern is relevant whenever you have a number of algorithms (or some combination of functions and inputs) that share some common behavior. Put another way, try using this pattern whenever you have a single Goal to accomplish with a number of Approaches. Here are some concrete examples, with the Goal and Approaches highlighted:
- You know two different ways to generate an audio tone, each with benefits and drawbacks
- Goal: Generate an audio tone
- Approaches: fixed-size buffer, dynamic buffer
- You want to “clean” various types of data based on different rules, each with different “fallback” values
- Goal: Ensure data is within expected bounds
- Approaches: Names of functions, frequencies between 1 and 20,000, buffer sizes that are powers of 2
- You have a number of buttons, and you want each to have a unique label and response when clicked.
- Goal: Create a functional button
- Approaches: log in, log out, get contacts
Implementation
That last example is exactly what Boaz discussed in his most recent blog post. Let’s use a stripped-down version of his code to see how simple this pattern can be:
boaz_strategy.js
var buttons = {
login: {
label: 'Login to Google',
action: function() {
google.accounts.user.login('https://www.google.com/m8/feeds');
}
},
logout: {
label: 'Logout from Google',
action: function() {
google.accounts.user.logout();
}
},
getContacts: {
label: 'Get contacts',
action: function() {
var contactsService = new google.gdata.contacts.ContactsService( 'Contacts Viewer' ),
query = new google.gdata.contacts.ContactQuery( 'https://www.google.com/m8/feeds/contacts/default/full' );
query.setMaxResults( $('#numContacts').val() );
contactsService.getContactFeed(
query,
function( result ) {
$('#contacts').remove();
var $contactsHolder = $('<ul>', {
id: 'contacts'
});
$.each( result.feed.entry, function( i, entry ){
$.each( entry.getEmailAddresses(), function( j, address ){
$contactsHolder.append( '<li>' + address.address + '</li>' );
});
});
$contactsHolder.appendTo( 'body');
},
function( result ) {
// Log the error
console.log('error: ', result);
}
);
}
}
};
(You can see his inspiration in this Gist from Rick, where the handlers
object holds dummy Approaches.) Each property of the buttons
object represents a unique button
. This code recognizes the common aspects (the Goals: label the button and perform some action) that are shared by each unique button (the Approaches: log in, log out, get contacts). Now that this relationship has been set up, we can leverage its representational power:
boaz_leverage.js
$.each( buttons, function( propertyName, button ) {
$('<button>', {
html: button.label,
id: propertyName
})
.bind('click', button.action)
.appendTo( 'nav' );
});
This code leverages the common interface (Goals) we teased out of each button (Approaches). We simply iterate over the controls object, confident that each member has some label and action. In this way, we’ve saved ourselves from having to write blocks of redundant code (you can see what I mean here). Adding new buttons is also much easier because you only need to define the unique aspect of each—no need to retrace through the code adding logic for binding functions to buttons, etc.
Making it Robust
Although this is perfectly serviceable, there are steps we can take to make sure every Approach conforms to the same standard. Simply define a generic approach for the others to inherit from:
robust.js
var Button = function(opts) {
for( var attr in opts ) {
if(opts.hasOwnProperty(attr)) {
this[attr] = opts[attr];
}
}
};
Button.prototype.label = 'button';
Button.prototype.action = function() {};
Using this Button
object admittedly adds a small amount of code to the buttons
definition, for example: getContacts: { /* ... */ }
becomes getContacts: new Button({ /* ... */ })
(See here for the complete definition.) In return, we’ve built a clear contract of what each Button
provides.
Runtime
So far, I have motivated the use of this pattern for object instantiation. While this makes code more readable and maintainable, it still might not be clear how this increases the power of the code. Consider another example (as mentioned earlier, working with audio tones):
runtime.js
var waveImplementations = {
discrete: new Wave({
node: context.createBufferSource(),
is_initialized: false,
init: function() { /* ... */ },
readData: function( channel, callback ) { /* ... */ },
connect: function( target ) { /* ... */ },
disconnect: function() { /* ... */ }
}),
continuous: new Wave({
node: context.createJavaScriptNode( waveForm.bufferSize, 0, 1 ),
is_initialized: false,
callback: noop,
init: function() { /* ... */ },
readData: function( channel, callback ) { /* ... */ },
connect: function( target ) { /* ... */ },
disconnect: function() { /* ... */ }
})
},
wave = waveImplementations.discrete;
Once again, the data structures and methods unique to each approach have been teased out into dedicated objects. By defining the wave
object in this way, the rest of the code can be written without any regard for the unique implementation details of continuous
and discrete
waves. More importantly, we can switch implementations at any time with one simple line of code: wave = waveImplementations.continuous;
Comments
We moved off of Disqus for data privacy and consent concerns, and are currently searching for a new commenting tool.
You should take a look at KnockoutJS(http://www.knockoutjs.com), you write code like this(a viewmodel) and it has automatic binding 🙂
\”Design patterns are derived from scrutiny of best practices in the real world\”
If you take out the word \”best\” there, you’re correct. Design patterns are merely descriptions, not prescriptions.
Give jQuery Condom a try. It may help you a bit 🙂
https://github.com/kuroir/j…
This is pretty cool. Thanks for writing the post about it. I really like the idea behind Button ‘interface’ which standardizes api for all buttons. In case we have to deal with multiple strategies in our project it would be nice to maybe create a generic ‘Strategy’ which would do what Button constructor does now. What do you think?
Alisson & Mario:
Thanks for the resources–I’m always interested in checking out new API’s 😛
Michal:
You could definately create a generic ‘Strategy’, depending on your needs. Each interface you create will invariably be more generic than its children, so at some point you have to question how much it is actually helping! You can always consider the Object class as your most generic interface (for functions like hasOwnProperty(), toString(), etc.)
I find myself using this approach a lot (it just makes sense) but until reading this, didn’t know what to call it. thanks!
It seems to work really well in Backbone.js, along with the model/view bindings. I see it as defining a Collection in as simple a form as possible, then instantiating each of them as you did with the Buttons above.
Great writeup!
I looking the the code in the Gist from Rick Link: https://gist.github.com/898710
(function( global ) {
var Dogfood = (function() {
// Handlers => Menu Items
// IRL, these would have handler functions
var handlers = {
\”add-new\”: true,
\”reply\”: true,
\”reply-all\”: true,
\”forward\”: true,
\”more-action\”: true
},
dummy = function( action, data ) {
console.log( action, data );
};
return {
init: function( selector ) {
var actions = _.keys( handlers ),
domFrags = {
li: $( \”<li>\” ),
a: $( \”\” )
},
$stack = $(false);
_.each( actions, function( action ) {
// Create new elements from cached frags
var $li = domFrags.li.clone(),
$a = domFrags.a.clone();
// Set up the anchor
$a.attr({
\”href\”: \”/\” + action
}).html( action );
// Attach data to the li
$li.data( \”action\”, action );
// Push the new frags into the collection
$stack.push( $li.append( $a )[0] );
// Demo only – Since no handlers are provided below
handlers[ action ] = dummy;
}, this );
// Bind click handlers
$stack.bind( \”click\”,
this.event.click
).appendTo( selector );
},
event: {
click: function( event, special ) {
// Whoa.
event.preventDefault();
var action = $(this).data( \”action\” );
handlers[ action ]( action, event.data || special || {} );
}
},
handlers: handlers
};
})();
global.Dogfood = Dogfood;
})( this );
$(function() {
Dogfood.init( \”#ui-menu-stuff\” );
});
I don’t like this dummy approach he used as for someone with my level of experience, I am not sure the how he intends to add real handlers. Can someone show me how, assume I am an idiot over assuming I know everything you know.
The confusing part to me is the conflicting (to me) comments:
// IRL, these would have handler functions
//Demo only as there are no handlers provided below.
So do I add them above or below? I tried replacing some of the true with functions I created but the handlers[action] = dummy overwrote it. But if I comment out the line handlers[action] = dummy it doesn’t work at all.
It makes sense to replace the trues with function names, but it doesn’t seem like that is the intent. That is why I don’t like dummy examples.
There isn’t anything to explain about the comment, re: real life handlers. The example was mean to explain an idea, everything else requires a bit of imagination.
To make it easier to grok, I updated it to include concrete (but still dummy) handler definitions