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:
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:
$.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:
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):
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;