⬆️ ⬇️

Javascript Strategy Pattern

From the translator:

I was going to study the new Strategy pattern for me, but I did not find an intelligent Russian description of its implementation in javascript. The article on the wiki is frightening in its complexity, and the clarity of the example leaves much to be desired. That's why I took up the translation of this article, at the same time figuring out what this pattern is.

Spoilers and text highlighted in gray are my comments.

Next, we'll look at examples of how I use STRATEGY in Javascript, and how it is used by the real library, to break it down into small pieces.


I adore the Strategy pattern. I try to use it wherever possible. In essence, it uses delegation to separate the algorithms from the classes that use them.



There are several advantages to this approach.

Firstly, it allows to avoid complex conditional constructions for choosing which variant of the algorithm to use.

Secondly, it weakens connectivity , thereby reducing the complexity of clients, and encourages the rejection of the use of subclasses in favor of class aggregation .

Third, it improves modularity and testability.



In the implementation of STRATEGY , two participants are usually used:

Hereinafter:

STRATEGY - pattern itself.

Strategy - a separate implementation of the algorithm.

')

Next, we'll look at examples of how I use STRATEGY in Javascript, and how it is used by the real library, to break it down into small pieces.



Strategy as a function

The built-in FUNCTION class provides a great way to encapsulate an algorithm. This means that functions can be used as strategies . Just pass the function to the client and make sure that the client uses it.



We illustrate this with an example. Suppose we want to create a class Greeter . His task is to greet people. We also want Greeter to be able to greet them in different ways. That is, we need several different implementations of the algorithm . For this, we will create different greeting strategies. Hereinafter, the algorithm implies a greeting.

// Greeter -  ,    . //         var Greeter = function(strategy) { this.strategy = strategy; }; // Greeter   greet, //       //  ,    Greeter.prototype.greet = function() { return this.strategy(); }; //     , //       //  : var politeGreetingStrategy = function() { console.log("Hello."); }; var friendlyGreetingStrategy = function() { console.log("Hey!"); }; var boredGreetingStrategy = function() { console.log("sup."); }; //   ! 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. 
There is an error in this code (thanks to bashtannik ). Since the algorithms of the strategies already have the output in the console, and the greet method returns a function that returns nothing - the last three lines should be replaced by
these
 politeGreeter.greet(); //=> Hello. friendlyGreeter.greet(); //=> Hey! boredGreeter.greet(); //=> sup. 


In the example above, we created a Greeter client and three different strategies . Obviously, Greeter knows how to use the algorithm , but has no idea about what is under his hood.



But for complex algorithms, functions are often not enough. In this case, it is better to use the STRATEGY in the OOP style.



Strategy as a class

Classes can also be strategies , especially in cases where the algorithms are more complex than the one invented in the example above. Using classes allows you to define an interface for each strategy .



Consider this by example.

implies that this code is located before the example.
 var Greeter = function(strategy) { this.strategy = strategy; }; 
 //       Javascript //   ,      //     , //          . //    ,      var Strategy = function() {}; Strategy.prototype.execute = function() { throw new Error('Strategy#execute needs to be overridden.') }; //           //        `Strategy`. //  ,    , //      `execute` var GreetingStrategy = function() {}; GreetingStrategy.prototype = Object.create(Strategy.prototype); //   `execute`,       //    `Strategy`   . //  ,       . //      (Template Method). //      . GreetingStrategy.prototype.execute = function() { return this.sayHi() + this.sayBye(); }; GreetingStrategy.prototype.sayHi = function() { return "Hello, "; }; GreetingStrategy.prototype.sayBye = function() { return "Goodbye."; }; //      . //    -   `Greeter`. Greeter.prototype.greet = function() { return this.strategy.execute(); }; var greeter = new Greeter(new GreetingStrategy()); greeter.greet() //=> 'Hello, Goodbye.' 
We defined Strategy as an object (or class) with the execute method. The client can use any strategy that fits this class.



Pay attention to the GreetingStrategy . The most interesting thing is to override the execute method. It depends on other methods of this class. Now objects that inherit this class can change individual methods, such as sayHi or sayBye , without changing the basic algorithm . This pattern is called the Template Method and it combines perfectly with STRATEGY .



Let's see how.

 //   GreetingStrategy#execute    . //    ,     , //       ( `execute`) 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.' 
By defining the execute method, GreetingStrategy creates a family of algorithms . In the above fragment, we took advantage of this by creating several varieties of them.



Even without the use of subclasses, Greeter still has polymorphism . There is no need to switch to a different type of Greeter to call the algorithm we need. Now they are all in every new Greeter .

 var greeters = [ new Greeter(new BoredGreetingStrategy()), new Greeter(new PoliteGreetingStrategy()), new Greeter(new FriendlyGreetingStrategy()), ]; greeters.forEach(function(greeter) { //    `greeter`     //      . //      `greet`, //        . greeter.greet(); }); 




STRATEGY in the real code

One of my favorite examples of using STRATEGY is the Passport.js library.



Passport.js provides an easy way to manage authentication in node.js. It supports a large number of providers (Facebook, Twitter, Google, etc.), each of which is presented as a separate strategy.



The library is available in the form of an npm-package, as well as all its strategies. The programmer is free to decide which npm package to install in this particular case. Here is a code snippet that clearly shows how this works:

 //    http://passportjs.org var passport = require('passport') //      npm-. //     . , LocalStrategy = require('passport-local').Strategy , FacebookStrategy = require('passport-facebook').Strategy; // Passport      . 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); }); } )); // ,    Facebook 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 Passport.js library itself contains only a couple of simple authentication mechanisms. There is nothing in it but them and the Context . This architecture allows third-party programmers to easily implement their own authentication mechanisms without cluttering up the project.



Morality

The Strategy Pattern provides a way to increase the modularity and testability of your code. But this does not mean that it should be used everywhere with or without. It is also useful to use impurities to add functionality to objects at run time. And sometimes a fairly simple polymorphism in the style of the good old duck typing .



Anyway, the use of STRATEGY , in the first place, allows you to scale the code, avoiding the large overhead of architecture. This can be seen in the example of Passport.js, in which the use of this pattern contributes to painless addition of new strategies from other programmers.

Source: https://habr.com/ru/post/191480/



All Articles