Strategy Pattern

The pattern allows:

  • to build a neat architecture where many versions of an algorithm are required to perform the task;
  • to encapsulate conditional logic of switch statement;
  • to define the behavior of a class at run-time.

Strategy Pattern


The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.

PHP Example

Only 5 years ago semantic web standards seemed as utopia. Now you can find plenty of real appliances. For example, Google parses meta-information from your pages when it is provided as a Rich Snippet. Currently Google supports Microdata, Microformat and Rdf wrappers. What would you do if you are required to wrap user profiles into Rich Snippets? You can write a strategy object, which turns the profile into a Microformat hCard. Now the profile can be obtained as a RichSnippet. Requirement changed? They want another wrapper? Just add a new strategy.

<?php
/*
 * @category Design Pattern Tutorial
 * @package Strategy Sample
 * @author Dmitry Sheiko <me@dsheiko.com>
 * @link http://dsheiko.com
 */

class Profile
{
    public $data;
    protected $_strategy;

    public function  __construct(array $data)
    {
        $this->data = $data;
    }
    public function setStrategyContext(RichSnippetStrategyAbstract $strategy)
    {
        $this->_strategy = $strategy;
    }
    public function get() {
        return $this->_strategy->get($this);
    }

}


abstract class RichSnippetStrategyAbstract
{
    protected $_cardTemplate;
    protected $_propertyTpls;

    protected function _getReplacePairs($properties)
    {
        $replPairs = array();
        foreach ($properties as $property => $val) {
            if (is_array ($val)) {
                $val = $this->_process($val);
            }
            $replPairs['{' . $property . '}'] =
                strtr($this->_propertyTpls[$property], array('{value}' => $val));
        }
        return $replPairs;
    }

    protected function _process(array $data)
    {
        if (!isset ($data["template"]) || !isset ($data["properties"])) {
            throw new Exception('Input data structure is not correct');
        }
        return strtr($data["template"], $this->_getReplacePairs($data["properties"]));
    }

    public function get(Profile $context)
    {
        $card = $this->_process($context->data);
        return sprintf($this->_cardTpl, $card);
    }
}

class ProfileAsMicrodataStrategy extends RichSnippetStrategyAbstract
{
    protected $_cardTpl = '<div itemscope itemtype="http://data-vocabulary.org/Person">%s</div>';
    protected $_propertyTpls = array(
        'name' => '<span itemprop="name">{value}</span>',
        'nickname' => '<span itemprop="nickname">{value}</span>',
        'url' => '<a href="{value}" itemprop="url">{value}</a>',
        'address' => '<span itemprop="address" itemscope itemtype="http://data-vocabulary.org/Address">{value}</span>',
        'locality' => '<span itemprop="locality">{value}</span>',
        'region' => '<span itemprop="region">{value}</span>',
        'title' => '<span itemprop="title">{value}</span>',
        'org' => '<span itemprop="affiliation">{value}</span>');

}
class ProfileAsMicroformatStrategy extends RichSnippetStrategyAbstract
{
    protected $_cardTpl = '<div class="vcard">%s</div>';
    protected $_propertyTpls = array(
        'name' => '<span class="fn">{value}</span>',
        'nickname' => '<span class="nickname">{value}</span>',
        'url' => '<a href="{value}"class="url">{value}</a>',
        'address' => '<span class="adr">{value}</span>',
        'locality' => '<span class="locality">{value}</span>',
        'region' => '<span class="region">{value}</span>',
        'title' => ' <span class="title">{value}</span>',
        'org' => '<span class="org">{value}</span>');
}
class ProfileAsRdfStrategy extends RichSnippetStrategyAbstract
{
    protected $_cardTpl = '<div xmlns:v="http://rdf.data-vocabulary.org/#" typeof="v:Person">%s</div>';
    protected $_propertyTpls = array(
        'name' => '<span property="v:name">{value}</span>',
        'nickname' => '<span property="v:nickname">{value}</span>',
        'url' => '<a href="{value}" rel="v:url">{value}</a>',
        'address' => '<span rel="v:address"><span typeof="v:Address">{value}</span></span>',
        'locality' => '<span property="v:locality">{value}</span>',
        'region' => '<span property="v:region">{value}</span>',
        'title' => '<span property="v:title">{value}</span>',
        'org' => '<span property="v:affiliation">{value}</span>');
}

/**
 * Usage
 */
$profileData = array (
    'template' =>
        'My name is {name}, but friends call me {nickname}.'
        . ' Here is my home page: {url}.'
        . ' Now I live in {address} and work as a {title} at {org}.',
    'properties' => array (
        'name' => 'Dmitry Sheiko',
        'nickname' => 'Dima',
        'url' => 'http://dsheiko.com',
        'address' => array (
            'template' => '{locality}, {region}',
            'properties' => array (
                'locality' => 'Frankfurt am Main',
                'region' => 'Germany',
            )
        ),
        'title' => 'web-developer',
        'org' => 'Crytek',
    )
);
$profile = new Profile($profileData);
$profile->setStrategyContext(new ProfileAsMicrodataStrategy());
echo $profile->get(), "\n";
$profile->setStrategyContext(new ProfileAsMicroformatStrategy());
echo $profile->get(), "\n";

// Output
// -> <div itemscope itemtype="http://data-vocabulary.org/Person">My ...
// -> <div class="vcard">My name is <span class="fn">Dmitry Sheiko ...

JS/ES5 Example

We have a set of video player parameters. In order to render the video player based on them we can choose concrete strategy. So it can be rendered as HTML5 player or as Flash player.

/*
 * @category Design Pattern Tutorial
 * @package Strategy Sample
 * @author Dmitry Sheiko <me@dsheiko.com>
 * @link http://dsheiko.com
 */
(function() {

var VideoPlayer = function(data) {
    var _strategy = null,
        _data = data;
    return {
        data : _data,
        setStrategyContext : function(strategy) {
            _strategy = strategy;
        },
        render : function() {
            return _strategy.render(this);
        }
    }
}
var VideoPlayerViaHTML5 = function() {
    var _private = {
        getVideoEl : function(width, height, poster) {
            var videoEl = document.createElement('video');
            videoEl.setAttribute('controls', 'controls');
            videoEl.setAttribute('preload', 'none');
            videoEl.setAttribute('poster', poster);
            videoEl.setAttribute('width', width);
            videoEl.setAttribute('height', height);
            return videoEl;
        },
        getSourceEl : function(type, src) {
            var srcEl = document.createElement('source');
            srcEl.setAttribute('type', type);
            srcEl.setAttribute('src', src);
            return srcEl;
        }
    }
    return {
        render : function(context) {
            var videoEl = _private.getVideoEl(
                context.data.width, context.data.height, context.data.poster);
            videoEl.appendChild(_private.getSourceEl('video/mp4', context.data.mp4Src));
            videoEl.appendChild(_private.getSourceEl('video/webm', context.data.webmSrc));
            videoEl.appendChild(_private.getSourceEl('video/ogg', context.data.oggSrc));
            return videoEl;
        }
    }
}
var VideoPlayerViaFlash = function() {
    var FALLBACK_SWF = 'flashmediaelement.swf';
    var _private = {
        getObjectEl : function(width, height) {
            var objectEl = document.createElement('object');
            objectEl.setAttribute('type', 'application/x-shockwave-flash');
            objectEl.setAttribute('data', FALLBACK_SWF);
            objectEl.setAttribute('width', width);
            objectEl.setAttribute('height', height);
            return objectEl;
        },
        getParamEl : function(name, value) {
            var paramEl = document.createElement('param');
            paramEl.setAttribute('name', name);
            paramEl.setAttribute('value', value);
            return paramEl;
        }
    }
    return {
        render : function(context) {
            var objectEl = _private.getObjectEl(
                context.data.width, context.data.height);
            objectEl.appendChild(_private.getParamEl('movie', FALLBACK_SWF));
            objectEl.appendChild(_private.getParamEl('flashvars', 'controls=true&poster='
                + context.data.poster + '&file=' + context.data.mp4Src));
            return objectEl;
        }
    }
}

var context = new VideoPlayer({
    width : 320,
    height: 240,
    poster: 'poster.jpg',
    mp4Src: 'myvideo.mp4',
    webmSrc: 'myvideo.webm',
    oggSrc: 'myvideo.ogg'
});

if (window.navigator.userAgent.toLowerCase().match(/msie [0-8]\./i) !== null) {
    context.setStrategyContext(new VideoPlayerViaFlash());
} else {
    context.setStrategyContext(new VideoPlayerViaHTML5());
}
console.log(context.render());

}());