Explain JavaScript's strategy pattern programming in detail

  • 2020-06-19 09:44:33
  • OfStack

I like strategy design patterns. I try to use it as much as I can. At its core, policy patterns use delegates to decouple the algorithm classes that use them.

This has several advantages. It prevents the use of large conditional statements to determine which algorithms are used for a particular type of object. This separation of concerns reduces the complexity of the client while also facilitating the composition of subclassing. It improves modularity and testability. Each algorithm can be tested separately. Each client can simulate the algorithm. Any client can use any algorithm. They can intertune. It's like lego 1.

To implement the policy pattern, there are typically two actors:

The policy object encapsulates the algorithm.

The client (context) object can use any policy in a plug and play manner.

Here's how I used the policy pattern in Javascrip, how I used it to break libraries into widgets and plug-and-play packages in a chaotic environment.

Function as strategy

A function provides an excellent way to encapsulate an algorithm and can be used as a strategy. Just go through one function to the client and make sure your client can call the policy.

Let's do an example. Suppose we want to create an Greeter class. All it has to do is say hello. We want the Greeter class to know the different ways to greet people. To implement this idea, we created different strategies for greetings.


// Greeter is a class of object that can greet people.
// It can learn different ways of greeting people through
// 'Strategies.'
//
// This is the Greeter constructor.
var Greeter = function(strategy) {
this.strategy = strategy;
};
 
// Greeter provides a greet function that is going to
// greet people using the Strategy passed to the constructor.
Greeter.prototype.greet = function() {
return this.strategy();
};
 
// Since a function encapsulates an algorithm, it makes a perfect
// candidate for a Strategy.
//
// Here are a couple of Strategies to use with our Greeter.
var politeGreetingStrategy = function() {
console.log("Hello.");
};
 
var friendlyGreetingStrategy = function() {
console.log("Hey!");
};
 
var boredGreetingStrategy = function() {
console.log("sup.");
};
 
// Let's use these strategies!
var politeGreeter = new Greeter(politeGreetingStrategy);
var friendlyGreeter = new Greeter(friendlyGreetingStrategy);
var boredGreeter = new Greeter(boredGreetingStrategy);
 
console.log(politeGreeter.greet()); //=> Hello.
console.log(friendlyGreeter.greet()); //=> Hey!
console.log(boredGreeter.greet()); //=> sup.

In the above example, Greeter is the client and has three policies. As you can see, Greeter knows how to use the algorithm, but doesn't know the details of the algorithm.

For complex algorithms, a simple function is often not sufficient. In this case, the good way is to define it in terms of objects.

Class as a policy

Policies can also be classes, especially if the calculation is more complex than the artificial (policy/algorithm) used in the above example. Using classes allows you to define one interface for each policy.

In the following example, this point is confirmed.


// We can also leverage the power of Prototypes in Javascript to create
// classes that act as strategies.
//
// Here, we create an abstract class that will serve as the interface
// for all our strategies. It isn't needed, but it's good for documenting
// purposes.
var Strategy = function() {};
 
Strategy.prototype.execute = function() {
 throw new Error('Strategy#execute needs to be overridden.')
};
 
// Like above, we want to create Greeting strategies. Let's subclass
// our Strategy class to define them. Notice that the parent class
// requires its children to override the execute method.
var GreetingStrategy = function() {};
GreetingStrategy.prototype = Object.create(Strategy.prototype);
 
// Here is the `execute` method, which is part of the public interface of
// our Strategy-based objects. Notice how I implemented this method in term of
// of other methods. This pattern is called a Template Method, and you'll see
// the benefits later on.
GreetingStrategy.prototype.execute = function() {
 return this.sayHi() + this.sayBye();
};
 
GreetingStrategy.prototype.sayHi = function() {
 return "Hello, ";
};
 
GreetingStrategy.prototype.sayBye = function() {
 return "Goodbye.";
};
 
// We can already try out our Strategy. It requires a little tweak in the
// Greeter class before, though.
Greeter.prototype.greet = function() {
 return this.strategy.execute();
};
 
var greeter = new Greeter(new GreetingStrategy());
greeter.greet() //=> 'Hello, Goodbye.'

Using classes, we define a policy with the anexecutemethod object. The client can implement this interface using any policy.

Notice also how I created GreetingStrategy. The interesting part is the overloading of methodexecute. It is defined in the form of other functions. Descendants of a class can now change specific behavior, such as thesayHiorsayByemethod, without changing the regular algorithm. This pattern is called the template approach and fits well with the policy pattern.

Let's see.


// Since the GreetingStrategy#execute method uses methods to define its algorithm,
// the Template Method pattern, we can subclass it and simply override one of those
// methods to alter the behavior without changing the algorithm.
 
var PoliteGreetingStrategy = function() {};
PoliteGreetingStrategy.prototype = Object.create(GreetingStrategy.prototype);
PoliteGreetingStrategy.prototype.sayHi = function() {
 return "Welcome sir, ";
};
 
var FriendlyGreetingStrategy = function() {};
FriendlyGreetingStrategy.prototype = Object.create(GreetingStrategy.prototype);
FriendlyGreetingStrategy.prototype.sayHi = function() {
 return "Hey, ";
};
 
var BoredGreetingStrategy = function() {};
BoredGreetingStrategy.prototype = Object.create(GreetingStrategy.prototype);
BoredGreetingStrategy.prototype.sayHi = function() {
 return "sup, ";
};
 
var politeGreeter  = new Greeter(new PoliteGreetingStrategy());
var friendlyGreeter = new Greeter(new FriendlyGreetingStrategy());
var boredGreeter  = new Greeter(new BoredGreetingStrategy());
 
politeGreeter.greet();  //=> 'Welcome sir, Goodbye.'
friendlyGreeter.greet(); //=> 'Hey, Goodbye.'
boredGreeter.greet();  //=> 'sup, Goodbye.'

GreetingStrategy creates a class algorithm by specifying theexecutemethod. In the code snippet above, we take advantage of this point by creating a specialized algorithm.

Without subclasses, our Greeter still exhibits a polymorphic behavior. There is no need to switch between different types of Greeter to trigger the correct algorithm. This slice is bound to every Greeter object.


var greeters = [
 new Greeter(new BoredGreetingStrategy()),
 new Greeter(new PoliteGreetingStrategy()),
 new Greeter(new FriendlyGreetingStrategy()),
];
 
greeters.forEach(function(greeter) {
  
 // Since each greeter knows its strategy, there's no need
 // to do any type checking. We just greet, and the object
 // knows how to handle it.
 greeter.greet();
});

Policy patterns in multiple environments

One of my favorite examples of policy patterns is in the ES67en.js library. Passport.js provides a simple way to handle authentication in Node. A wide range of vendors support it (Facebook, Twitter, Google, etc.), each implemented as a policy.

The library works as an npm package, and all its policies are the same. Users of the library can decide which npm package to install for their particular use case. Here's a code snippet that shows how it works:


// Taken from http://passportjs.org
 
var passport = require('passport')
   
  // Each authentication mechanism is provided as an npm package.
  // These packages expose a Strategy object.
 , LocalStrategy = require('passport-local').Strategy
 , FacebookStrategy = require('passport-facebook').Strategy;
 
// Passport can be instanciated using any Strategy.
passport.use(new LocalStrategy(
 function(username, password, done) {
  User.findOne({ username: username }, function (err, user) {
   if (err) { return done(err); }
   if (!user) {
    return done(null, false, { message: 'Incorrect username.' });
   }
   if (!user.validPassword(password)) {
    return done(null, false, { message: 'Incorrect password.' });
   }
   return done(null, user);
  });
 }
));
 
// In this case, we instanciate a Facebook Strategy
passport.use(new FacebookStrategy({
  clientID: FACEBOOK_APP_ID,
  clientSecret: FACEBOOK_APP_SECRET,
  callbackURL: "http://www.example.com/auth/facebook/callback"
 },
 function(accessToken, refreshToken, profile, done) {
  User.findOrCreate(..., function(err, user) {
   if (err) { return done(err); }
   done(null, user);
  });
 }
));

The ES84en. js library is equipped with only one or two simple authentication mechanisms. In addition, it does not have an interface for more than 1 policy class that fits the context object. This mechanism makes it easy for its users to implement their own authentication mechanism without adversely affecting the project.

reflection

Policy patterns provide a way to increase modularity and testability in your code. This does not mean that the (policy pattern) always works. Mixins can also be used for functional injection, such as the algorithm of 1 object at runtime. The flat old ES91en-ES92en polymorphism can sometimes be simple enough.

However, using the policy pattern allows you to increase the size of your code as the load grows without introducing a large system in the first place. As we saw in the example of ES95en. js, it will be easier for maintainers to add additional strategies in the future.


Related articles: