JavaScript Strategy Pattern
Definition
Strategy - defines a family of algorithms, encapsulates each, and makes them interchangeable. Strategy lets the algorithm vary independently form clients that use it. [2]
It is based on few OO Principles:
- Encapsulates what varies
- Favor composition over inheritance
- Program to interfaces, not implementations
This pattern seems to be very similar to Factory, Command and others. The main difference is that it is one to many pattern (one object can have many strategies). Also this pattern is used to define algorithms. It may be used for:
- Using optimal sorting algorithm for different types of data
- Using different algorithms to count product discount based on user type
- Using different algorithms to convert an image/file to different format
- Views facilitated cy Controllers in MVC pattern
Basic Strategy Example
Lets take a simple example in which we want to output some information about passed in objects. We define 2 handlers for array and object. Also we define one default handler which will be used if no specific handler was set.
var Strategy = {
strategies: {
default: function(input) {
console.log('There is no handler for ' + typeof input + ' type')
}
, array: function(input) {
console.log('Array has ' + input.length + ' elements')
}
, object: function(input) {
console.log('Object string notation: ' + input.toString())
}
}
, action: function(input){
var type = Object.prototype.toString.call(input).match(/\[object (\w+)\]/i)[1].toLowerCase()
if (this.strategies[type]) {
this.strategies[type](input)
} else {
this.strategies.default(input)
}
}
}
Strategy.action({}) // Object string notation: [object Object]
Strategy.action([1, 2, 3]) // Array has 3 elements
Strategy.action('string') // There is no handler for string type
Strategy.action(true) // There is no handler for boolean type
Later if necessary we can add more handlers. But the main idea here is that algorithm which processes data is chosen in runtime.
Another example is to choose strategy by yourself:
var Strategy = function(type) {
if (this.strategies[type]) {
this.strategy = this.strategies[type]
} else {
this.strategy = this.strategies.default
}
}
Strategy.prototype.strategies = {
default: function(input) {
console.log('There is no handler for ' + typeof input + ' type')
}
, array: function(input) {
console.log('Array has ' + input.length + ' elements')
}
, object: function(input) {
console.log('Object string notation: ' + input.toString())
}
}
Strategy.prototype.action = function(input) {
this.strategy(input)
}
var s = new Strategy('array')
s.action({}) // Array has undefined elements
s.action([1, 2, 3]) // Array has 3 elements
s.action('string') // Array has 6 elements
s.action(true) // Array has undefined elements
Lets try one class-like example. For algorithms we’ll have one parent object Sort for all common functionality. Then we create as many Sort’s children as we need. If Sort object is empty (as in example) we can easily can get rid of it and of objects linking.
var Sort = {}
var BubbleSort = Object.create(Sort)
BubbleSort.sort = function(data) {
console.log('bubbling')
return data.sort()
}
var MergeSort = Object.create(Sort)
MergeSort.sort = function(data) {
console.log('merging')
return data.sort()
}
var Strategy = {
init: function(type) {
if (type === 'bubble')
this.sortAlgorithm = BubbleSort
else if (type === 'merge')
this.sortAlgorithm = MergeSort
}
, process: function(data) {
return this.sortAlgorithm.sort(data)
}
}
s1 = Object.create(Strategy)
s1.init('bubble')
s1.process([1,3,4,2])
s2 = Object.create(Strategy)
s2.init('merge')
s2.process([1,3,4,2])
CoffeeScript example
Lets start with classes:
class Sorting
sort: (data)->
data.sort(@algorithm)
algorithm: (a, b) ->
return a - b
class RandomSorting extends Sorting
algorithm: (a, b) ->
return [-1, 0, 1][new Date().getTime() % 3]
class ReversedSorting extends Sorting
algorithm: (a, b) ->
return b - a
class Strategy
constructor: (type) ->
switch type
when 'random' then @algorithm = new RandomSorting()
when 'reverse' then @algorithm = new ReversedSorting()
else @algorithm = new Sorting()
sort: (data) ->
return @algorithm.sort(data)
s = new Strategy('default')
console.log s.sort([2,5,1,3]) # [1, 2, 3, 5]
s2 = new Strategy('reverse')
console.log s2.sort([2,5,1,3]) # [5, 3, 2, 1]
s3 = new Strategy('random')
console.log s3.sort([2,5,1,3]) # [3, 1, 5, 2]
As in JavaScript we don’t care about polymorphism and functions are first-class objects we can use a more clear and simple Strategy pattern implementation:
SimpleSort = (data)->
data.sort()
ReversedSort = (data)->
data.sort (a, b) ->
return b - a
RandomSort = (data)->
data.sort (a, b) ->
return [-1, 0, 1][new Date().getTime() % 3]
Sorter = (algorithm) ->
sort: (list) -> algorithm list
s = new Sorter SimpleSort
console.log s.sort([2,5,1,3]) # [1, 2, 3, 5]
s2 = new Sorter ReversedSort
console.log s2.sort([2,5,1,3]) # [5, 3, 2, 1]
s3 = new Sorter RandomSort
console.log s3.sort([2,5,1,3]) # [3, 1, 5, 2]
Sources
- (github) shichuan / javascript-patterns / design-patterns / strategy.html
- (book) Head First Design Patterns
- (book) Learning JavaScript Design Patterns
- (book) JavaScript Patterns: Build Better Applications with Coding and Design Patterns
- (book) Learning JavaScript Design Patterns: A JavaScript and jQuery Developer's Guide
- (book) Pro JavaScript Design Patterns: The Essentials of Object-Oriented JavaScript Programming
- (book) CoffeeScript Cookbook
- (article) dofactory JavaScript Strategy Pattern
- (article) say what?
- (article) JS Objects: De”construct”ion
- (wiki) Design Pattern Encapsulation Hierarchy