JavaScript戦略パターン

以前、同じ名前の記事の翻訳を既に公開しました。 そして、彼女のaTei同志の下にコメントを残しました:


私の意見では、この記事とウィキペディアの記事に何かが欠けています-「悪い-良くなった」というスタイルの例です。 すぐに「良い」と判明し、それが本当に良いか十分に明確ではありません。 そのような例に感謝します。

これまでのところ、誰も答えを出していない。 3年間入力しました 体験 勇気と今、このコメントへの答えとして、私は自分のために戦略パターンについて書きたいと思います。


理論のパン粉は本文のどこかにあります。 しかし、記事のほとんどは、このパターンを適用する実用的な方法と、それを回避する方法に専念しています。


与えられた:あなたができるロガーを書く:



2番目の段落では単一の「インターフェース」を想定しているため、宛先を変更するためにロガー呼び出しが発生するすべての行を書き換える必要はありません。




代替案


最初に、戦略の兆候を意図的に避ける「決定」のための2つのオプションを紹介します。


機能的アプローチ


それを純粋な関数にしてみましょう:


まず、主な作業を実行する2つの関数が必要です。


 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; 

両方ともメイン機能を実行し、新しいcount値を返します。


第二に、それらを結合するある種の統一されたインターフェースが必要です。 そして、ここで最初の問題が発生します...純粋な関数は状態を保存しないため、外部変数に影響を与えず、他の副作用はありません-実質的に他のオプションはなく、メイン関数内の宛先の選択をハードコードします。 たとえば、次のように:


 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`); }; }; 

クライアントコードで必要な変数をすべて宣言したら、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'); 

このアプローチの欠点は明らかだと思います。 ただし、最も重要なこと、ロガーコードを変更せずに新しい宛先を追加するという3番目の条件を満たさないことです。 結局、新しい宛先を追加したら、それをswitch(options.destination)追加する必要があります。


結果。 [結果]タブに切り替える前に、DevToolsコンソールをオンにします



OOPアプローチ


前回は、状態を保存できないことに制約されていたため、ロガーが動作するために必要な環境をクライアントコードが作成および維持することを要求しました。 OOPスタイルでは、インスタンスまたはクラスのプロパティで、この「内部」をすべて隠すことができます。


Loggerと連携するために、抽象クラスを作成します。このクラスでは、高レベルのメソッドlogwarnerrorについて説明しerror


さらに、 countプロパティが必要ですLoggerプロトタイププロパティとオブジェクトを作成してグローバルにし、インスタンスを持つサブクラスでプロトタイプを作成し、コピーを作成しませんでした。宛先ごとに異なるカウンターは必要ありませんか?)


 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}; 

前回と同様に2つの「主力馬」:


 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>`;} }; 

ここで、宛先を変更するために、ロガーのインスタンスを再定義して、異なるクラスから作成するだけです。


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

このオプションには機能的なアプローチがなくなりました-宛先を独立して書き込むことができます。 しかし、順番に、最後の条件を満たしていません: いくつかの独立したロガーを使用します。 Loggerクラスの静的プロパティにcountを保存するため。 したがって、すべてのインスタンスには1つの共通count


結果。 [結果]タブに切り替える前に、DevToolsコンソールをオンにします



戦略


実際、私は問題の条件を作りだましました。それらすべてを満足させるソリューションは、何らかの形で戦略パターンを実装します。 結局のところ、彼の主なアイデアは、メソッド(通常は「内部」)の実装を個別の完全に独立したエンティティに分離するようにコードを編成することです。 それで



ダーティ関数戦略


Logger関数の純度を放棄し、クロージャーを使用すると、次の解決策が得られます。


 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); }; }; 

logToConsoleおよびlogToDOMは同じままです。 あとはLoggerインスタンスを宣言するだけです。 そして、宛先を置き換えるには、必要なものをこのインスタンスに転送します。


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

結果。 [結果]タブに切り替える前に、DevToolsコンソールをオンにします



プロトタイプ戦略


最後の投稿の下で、同志tenshiは提案しました:


また、作業中にLocalPassportをFaceBookPassportに変更できないのはなぜですか?

次の実装のアイデアを投げたもの。 プロトタイプの継承は、驚くほど強力で柔軟なものです。 そして、 .__proto__プロパティの合法化により.__proto__魔法のようです。 インスタンスの継承元のクラス(プロトタイプ)を即座に変更できます。


この詐欺を使用します。


 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);} }; 

はい、ロガーの各インスタンスに正直にcountできます。


LogToConsoleは、 this.countではなくthis.count.value呼び出すことによってのみ異なります。 ただし、 LogToDomは大幅に変更されます。 このクラスのインスタンスを作成しないため、 constructorを使用して.domNodeを設定することはできなくなりました。 このためにセッター.setDomNode(node)メソッドを作成しましょう。


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

ここで宛先を変更するには、インスタンスのプロトタイプを置き換えるsetDestinationメソッドを呼び出す必要があります。


 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'); 

結果。 [結果]タブに切り替える前に、DevToolsコンソールをオンにします



インターフェース戦略


「Pattern Strategy」をグーグルで検索すると、記事のいずれか*にインターフェースの言及があります。 そして、他のどの言語でも、インターフェースは特定のユニークな機能を備えた特定の構文構成要素であるということが起こりました。 JSとは異なり、このような理由で、このパターンを当時に伝えるのは非常に難しかったようです。 (はい、私は誰をからかっているのでしょうか?


簡単な方法の場合: インターフェースを使用すると、特定のメソッドを持つように実装を「義務付ける」ことができます。 これらのメソッドの実装方法に関係なく。 たとえば、 クラスでは、 インターフェイスはメソッドで宣言されています。 また、 特定のインスタンスは、このインターフェースの異なる実装を使用できます: さらに、それらを時々変更します。 そのため、実装がで「オン」になると、 はメソッドを使用してインターフェースに 「こんにちは」と言います。 また、 オンになっている場合、同じアクションにより「Hello」と言うように促されます。


インターフェイスを使用して、このパターンの例を「クラシック」形式で提供せざるを得ませんでした。 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 . 

このアプローチは、以前のアプローチに非常に近いものになります。 Loggerコードでは、 destination関連付けられた行のみがjsInterfaceの行に置き換えられ、 writeメソッドがloginterfaceプロパティに転送されます。


 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); } }; 

上記のコードについて説明します。 コンストラクターで、インスタンスでnew Loggerを宣言します 財産 writeメソッドを備えたloginterfaceインターフェース。


LogToConsoleはデータストレージを必要としないため、 writeメソッドを使用して単純なlog2consoleオブジェクトにします。


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

ただし、 LogToDOMはストレージnodeが必要です。 確かに、不要なプロパティとメソッドを使用してLoggerインスタンスを混乱させることなく、クロージャでラップできるようになりました。


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

使用方法も以前のバージョンと非常に似ています。 さらに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'); 

あなたはおそらくそのような奇妙なことに気づいた:後


 logger.loginterface = log2console; 

This.countはthis.countはずです。 結局:


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

しかし、これはインターフェイスの魔法です。 実装は「独立した」オブジェクトではありません-このインターフェイスが宣言されている「実際の」オブジェクトが使用するメソッドのコードのみを提供します。 したがって、変換のチェーンは次のようになります。


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

結果。 [結果]タブに切り替える前に、DevToolsコンソールをオンにします



まとめ


戦略は基本的なパターンの1つです。 教科書の戒めに意識的に従わずに、しばしば直感的に実現されるもの。


他の言語については言いませんが、JSは非常に柔軟です! これは、他のパターンと同様に、構文に組み込まれていません。便利で便利な場所に実装してください。


もちろん、上記の3つは、このパターンのすべての可能な実装からはほど遠いものです。 読者であるあなたが他の多くの方法で同じことをできると確信しています。 それで、私はあなたが戦略のアイデアに注意することをお勧めします。それを実装しようとする私の哀れな試みではありません。




*私は本当に大好き 外挿する 誇張する



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


All Articles