以前、同じ名前の記事の翻訳を既に公開しました。 そして、彼女のaTei同志の下にコメントを残しました:
私の意見では、この記事とウィキペディアの記事に何かが欠けています-「悪い-良くなった」というスタイルの例です。 すぐに「良い」と判明し、それが本当に良いか十分に明確ではありません。 そのような例に感謝します。
これまでのところ、誰も答えを出していない。 3年間入力しました 体験 勇気と今、このコメントへの答えとして、私は自分のために戦略パターンについて書きたいと思います。
理論のパン粉は本文のどこかにあります。 しかし、記事のほとんどは、このパターンを適用する実用的な方法と、それを回避する方法に専念しています。
与えられた:あなたができるロガーを書く:
-
log
、warn
、error
の3つのレベルのログを書き込みerror
- ログの宛先を選択:コンソール、ページ(わかりやすくするために選択)
- 一度
- 何回も
- ロガーコードを変更せずに新しい宛先を追加します。 たとえば、ファイル、ftp、mysql、mongoなど。
- ログエントリに番号(呼び出しの数)を追加する
- 複数の独立したロガーを使用する
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と連携するために、抽象クラスを作成します。このクラスでは、高レベルのメソッドlog
、 warn
、 error
について説明し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つは、このパターンのすべての可能な実装からはほど遠いものです。 読者であるあなたが他の多くの方法で同じことをできると確信しています。 それで、私はあなたが戦略のアイデアに注意することをお勧めします。それを実装しようとする私の哀れな試みではありません。
*私は本当に大好き 外挿する 誇張する