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

  1. (github) shichuan / javascript-patterns / design-patterns / strategy.html
  2. (book) Head First Design Patterns
  3. (book) Learning JavaScript Design Patterns
  4. (book) JavaScript Patterns: Build Better Applications with Coding and Design Patterns
  5. (book) Learning JavaScript Design Patterns: A JavaScript and jQuery Developer's Guide
  6. (book) Pro JavaScript Design Patterns: The Essentials of Object-Oriented JavaScript Programming
  7. (book) CoffeeScript Cookbook
  8. (article) dofactory JavaScript Strategy Pattern
  9. (article) say what?
  10. (article) JS Objects: De”construct”ion
  11. (wiki) Design Pattern Encapsulation Hierarchy