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:
log
, warn
and error
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.
First, I will give two options for the "solution" of intentionally avoiding signs of the Strategy.
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
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
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
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
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
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
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
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