📜 ⬆️ ⬇️

Pattern Strategy JavaScript

Earlier I published a translation of an article with the same title. And under it, comrade aTei left a comment :


In my opinion, something is missing in this article and in an article in Wikipedia - an example in the style “It was bad - it became good”. Immediately it turns out "good" and it is not clear enough that this is really good. I would be grateful for this example.

So far no one has given an answer to it. For 3 years I plucked up of experience courage and now, as a response to this comment, I want to write about the Strategy pattern on my own behalf.


Crumbs of the theory are found somewhere in the text. But most of the article is devoted to practical ways to use this pattern and options for its use to avoid.


Given: write Logger, which allows:



The second paragraph assumes a single "interface" that would not have to rewrite all the lines where the Logger call occurs for the sake of changing the destination.




Alternatives


First, I will give two options for the "solution" of intentionally avoiding signs of the Strategy.


Functional approach


Let's try to do this with pure functions :


First, we need two functions that will perform the main work:


 const logToConsole = (lvl,count,msg) => console[lvl](`${count++}: ${msg}`) || count; const logToDOM = (lvl,count,msg,node) => (node.innerHTML += `<div class="${lvl}">${count++}: ${msg}</div>`) && count; 

Both of them perform their main function, and then return the new value of count .


Secondly, we need some kind of a unified interface that unites them. And this is where we encounter the first problem ... Since pure functions do not store states, cannot influence external variables, and do not have other side effects - we have practically no other options but to choose the destination within the main function. For example:


 const Logger = (options) => { switch(options.destination){ case 'console': return logToConsole; case 'dom': return (...args) => logToDOM.apply(null,[...args,options.node]); default: throw new Error(`type '${type}' is not availible`); }; }; 

Now, having declared all the necessary variables in the client code, we can use our Logger:


 let logCount = 0; const log2console = {destination: 'console'}; const log2dom = { destination: 'dom' ,node: document.querySelector('#log') }; let logdestination = log2console; logCount = Logger(logdestination)('log',logCount,'this goes to console'); logdestination = log2dom; logCount = Logger(logdestination)('log',logCount,'this goes to dom'); 

I think the shortcomings of this approach are obvious. But the most important is that it does not satisfy the third condition: Add new destinations without making changes to the Logger code. After all, adding a new destination, we must make it to switch(options.destination) .


Result. Turn on the DevTools console before switching to the Result tab



OOP approach


The previous time we were constrained by the inability to store states, which is why we demanded that the client code create and maintain the environment that our Logger needs to work. In OOP style, we can hide all this "under the hood" - in the properties of instances or classes.


Let's create an abstract class in which, for the convenience of working with our Logger, we will describe high-level methods: log , warn and error .


In addition, we need the count property (I made it the property of the Logger prototype and an object to be global, and the subclasses with instances inherit it from the prototype and not create our own copy. Do we not need different counters for different destinations?)


 class Logger { log(msg) {this.write('log',msg);} warn(msg) {this.write('warn',msg);} error(msg) {this.write('error',msg);} }; Logger.prototype.count = {value:0}; 

and 2 workhorses like last time:


 class LogToConsole extends Logger { write(lvl, msg) {console[lvl](`${this.count.value++}: ${msg}`);} }; class LogToDOM extends Logger { constructor(node) { super(); this.domNode = node; } write(lvl,msg) {this.domNode.innerHTML += `<div class="${lvl}">${this.count.value++}: ${msg}</div>`;} }; 

Now we just have to redefine the Logger instance, creating it from different classes to change the destination:


 let logger = new LogToConsole; logger.log('this goes to console'); logger = new LogToDOM(document.querySelector('#log')); logger.log('this goes to dom'); 

This option no longer has a lack of a functional approach - it allows you to write destination independently. But, in turn, does not satisfy the last condition: Use several independent Loggers. As it stores count in a static property of the class Logger . So all instances will have one common count .


Result. Turn on the DevTools console before switching to the Result tab



Strategy


In fact, I cheated by composing the conditions of the problem: Any solution that satisfies them all will implement the Strategy pattern in one form or another. After all, its main idea is to organize the code in such a way as to isolate the implementation of any methods (usually “internal”) into a separate, absolutely independent entity. In the way that



Strategy on dirty functions


If we abandon the purity of the Logger function and use the closure, we get the following solution:


 const Logger = () => { var logCount = 0; var logDestination; return (destination,...args) => { if (destination) logDestination = (lvl,msg) => destination(lvl,logCount,msg,...args); return (lvl,msg) => logCount = logDestination(lvl,msg); }; }; 

The logToConsole and logToDOM remain the same. It remains only to declare a copy of the Logger. And to replace the destination - to transfer necessary to this instance.


 const logger = Logger(); logger(logToConsole)('log','this goes to console'); logger(logToDOM,document.querySelector('#log')); logger()('log','this goes to dom'); 

Result. Turn on the DevTools console before switching to the Result tab



Prototype strategy


Under the last post, comrade tenshi suggested:


And what prevents to change LocalPassport to FaceBookPassport while working?

Than threw the idea for the next implementation. Prototype inheritance is a surprisingly powerful and flexible thing. And with the legalization of properties .__proto__ - just magical. We can change the class (prototype) from which our instance is inherited on-the-go.


Let's use this fraud:


 class Logger { constructor(destination) { this.count = 0; if (destination) this.setDestination(destination); } setDestination(destination) { this.__proto__ = destination.prototype; }; log(msg) {this.write('log',msg);} warn(msg) {this.write('warn',msg);} error(msg) {this.write('error',msg);} }; 

Yes, now we can honestly put count in every instance of Logger.


LogToConsole will differ only by calling this.count instead of this.count.value . But LogToDom will change significantly. Now we can not use the constructor to set .domNode , because we will not create an instance of this class. For this we will make the setter method .setDomNode(node) :


 class LogToDOM extends Logger { write(lvl,msg) {this.domNode.innerHTML += `<div class="${lvl}">${this.count++}: ${msg}</div>`;} setDomNode(node) {this.domNode = node;} }; 

Now, to change the destination, call the setDestination method setDestination which will replace the prototype of our instance:


 const logger = new Logger(); logger.setDestination(LogToConsole); logger.log('this goes to console'); logger.setDestination(LogToDOM); logger.setDomNode(document.querySelector('#log')); logger.log('this goes to dom'); 

Result. Turn on the DevTools console before switching to the Result tab



Interface strategy


If you google "Pattern Strategy", then in any of the articles you will find mention of interfaces. And it so happened that in any * other language: the interface is a specific syntactic construct with specific unique functionality. Unlike JS ... It seems to me that it was for this reason that this pattern was so hard for me at the time. (Yes, who am I kidding? Until now, how does it work?)


If it is simple: The interface allows you to "oblige" the implementation (implementation) to have specific methods. Regardless of how these methods are implemented. For example, in the class , the interface is declared with methods to and . And a specific instance of can use different implementations of this interface: , , . And even change them from time to time. So with the "included" implementation of , our using the method to interface - will say "Hello" . And when turned on, the same action prompts him to say 'Hello' .


I could not refrain from giving an example of this pattern in its “classic” form using interfaces. Why sketched a small library that implements the concept of interfaces in JS - js-interface npm


Very brief educational program according to the syntax that will be used in the example:
 const MyInterface = new jsInterface(['doFirst','doSecond']); //     .doFirst(..)  .doSecond(..) MyInterface(object,'prop'); //   .prop  . //  Object.keys(object.prop) -> ['doFirst','doSecond'] * object.prop = implementation; // /   . // implementation    .    - ,    doFirst  doSecond . 

This approach will be very close to the previous one. In the Logger code, only the strings associated with the destination replaced with the one with jsInterface, and the write method is transferred to the loginterface property:


 class Logger { constructor() { this.count = 0; jsInterface(['write'])(this,'loginterface'); } log(msg) { return this.loginterface.write('log',msg); } warn(msg) { return this.loginterface.write('warn',msg); } error(msg) { return this.loginterface.write('error',msg); } }; 

I will explain the code above. In the constructor, we declare an instance of new Logger property interface loginterface with write method.


LogToConsole does not need to store any data for itself, so log2console make it a simple log2console object with the write method:


 const log2console = { write:function(lvl,msg) {console[lvl](`${this.count++}: ${msg}`);} }; 

But LogToDOM needs to store the node . True, it can now be wrapped in a closure and not clutter up the Logger instance with unnecessary properties and methods.


 function LogToDOM(node) { this.write = function(lvl,msg) {node.innerHTML += `<div class="${lvl}">${this.count++}: ${msg}</div>`;} }; 

The use is also very similar to the previous version. Unless it is not necessary to cause additional setDomNode .


 const logger = new Logger(); logger.loginterface = log2console; logger.log('this goes to console'); logger.loginterface = new LogToDOM(document.querySelector('#log')); logger.log('this goes to dom'); 

You've probably noticed this oddity: After


 logger.loginterface = log2console; 

must be beaten this.count . after all:


 logger.log('bla bla') -> -> this.loginterface.write('log','bla bla') -> -> log2console.write('log','bla bla') this.count === log2console.count 

But this is also the "magic" of interfaces. Implementations are not "independent" objects - they only provide the code of their methods for using "real" objects for which this interface is declared. So the chain of transformations will be as follows:


 logger.log('bla bla') -> -> this.loginterface.write('log','bla bla') -> -> log2console.write.apply(logger,['log','bla bla']) this.count === logger.count 

Result. Turn on the DevTools console before switching to the Result tab



Total


Strategy is one of the basic patterns. This, which is often implemented intuitively, without consciously following the commandments of any textbook.


I won't say for other languages, but JS is pretty darn flexible! This, like other patterns, is not embedded in the syntax - implement it as it is convenient and where it is convenient.


Of course, the 3 described above are not all possible implementations of this pattern. I am more than sure that you, the reader, can do the same in a dozen other ways. So I urge you to note exactly the idea of ​​the Strategy, and not my pathetic attempts to implement it.




* I love very much extrapolate exaggerate


')

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


All Articles