JavaScriptデザインパターンの包括的なガイド

公開: 2022-03-11

優れたJavaScript開発者として、あなたはクリーンで健全で保守可能なコードを書くよう努めています。 ユニークではありますが、必ずしもユニークなソリューションを必要としない興味深い課題を解決します。 以前に処理したまったく異なる問題の解決策に似たコードを書いていることに気付いたかもしれません。 あなたはそれを知らないかもしれませんが、あなたはJavaScriptデザインパターンを使用しました。 デザインパターンは、ソフトウェア設計で一般的に発生する問題に対する再利用可能なソリューションです。

JavaScriptデザインパターンの包括的なガイド

どの言語の存続期間中も、そのような再利用可能なソリューションの多くは、その言語のコミュニティの多数の開発者によって作成およびテストされています。 多くの開発者のこの組み合わせた経験のおかげで、このようなソリューションは、目前の問題を解決すると同時に、最適化された方法でコードを書くのに役立つため、非常に便利です。

デザインパターンから得られる主な利点は次のとおりです。

  • これらは実証済みのソリューションです。デザインパターンは多くの開発者によって使用されることが多いため、確実に機能することができます。 それだけでなく、それらが複数回改訂され、おそらく最適化が実装されたことを確信できます。
  • それらは簡単に再利用できます。デザインパターンは、特定の問題に結び付けられていないため、複数の特定の問題を解決するために変更できる再利用可能なソリューションを文書化します。
  • それらは表現力豊かです。デザインパターンは、大規模なソリューションを非常にエレガントに説明できます。
  • コミュニケーションが容易になります。開発者がデザインパターンに精通していると、特定の問題に対する潜在的な解決策について、より簡単にコミュニケーションをとることができます。
  • これらはコードのリファクタリングの必要性を防ぎます。アプリケーションがデザインパターンを念頭に置いて作成されている場合、特定の問題に正しいデザインパターンを適用することはすでに最適であるため、後でコードをリファクタリングする必要がない場合がよくあります。解決。
  • コードベースのサイズを小さくします。デザインパターンは通常、エレガントで最適なソリューションであるため、通常、他のソリューションよりも必要なコードが少なくて済みます。

この時点でジャンプする準備ができていることはわかっていますが、デザインパターンについてすべてを学ぶ前に、JavaScriptの基本を確認しましょう。

JavaScriptの簡単な歴史

JavaScriptは、今日のWeb開発で最も人気のあるプログラミング言語の1つです。 これは当初、初期のWebブラウザーの1つで、クライアント側のスクリプト言語として知られる、表示されるさまざまなHTML要素の一種の「接着剤」として作成されました。 Netscape Navigatorと呼ばれ、当時は静的HTMLしか表示できませんでした。 ご想像のとおり、このようなスクリプト言語のアイデアは、当時のブラウザ開発業界の大手企業であるNetscape Communications(現在のMozilla)、Microsoftなどの間でブラウザ戦争を引き起こしました。

大手企業はそれぞれ、このスクリプト言語の独自の実装を推進したいと考えていたため、NetscapeはJavaScriptを作成し(実際にはBrendan Eichが作成しました)、MicrosoftはJScriptを作成しました。 ご覧のとおり、これらの実装の違いは非常に大きかったため、Webブラウザーの開発はブラウザーごとに行われ、Webページに付属する最も見やすいステッカーが使用されました。 開発プロセスを統合し、Webページの作成を簡素化する標準のクロスブラウザソリューションが必要であることがすぐに明らかになりました。 彼らが思いついたのはECMAScriptと呼ばれています。

ECMAScriptは、すべての最新のブラウザーがサポートしようとする標準化されたスクリプト言語仕様であり、ECMAScriptには複数の実装(方言と言えます)があります。 最も人気のあるものは、この記事のトピックであるJavaScriptです。 ECMAScriptは最初のリリース以来、多くの重要なことを標準化してきました。詳細に関心のある人のために、ウィキペディアでECMAScriptの各バージョンの標準化された項目の詳細なリストがあります。 ECMAScriptバージョン6(ES6)以降のブラウザーのサポートはまだ不完全であり、完全にサポートするにはES5にトランスパイルする必要があります。

JavaScriptとは何ですか?

この記事の内容を完全に理解するために、JavaScriptデザインパターンに飛び込む前に知っておく必要のあるいくつかの非常に重要な言語特性を紹介しましょう。 誰かがあなたに「JavaScriptとは何ですか?」と尋ねた場合。 あなたは次の行のどこかに答えるかもしれません:

JavaScriptは、軽量で解釈されたオブジェクト指向プログラミング言語であり、Webページのスクリプト言語として最も一般的に知られているファーストクラスの関数を備えています。

前述の定義は、JavaScriptコードのメモリフットプリントが低く、実装が簡単で、習得が容易であり、C++やJavaなどの一般的な言語と同様の構文を使用していることを意味します。 これはスクリプト言語であり、コードがコンパイルされるのではなく解釈されることを意味します。 手続き型、オブジェクト指向、および関数型プログラミングスタイルをサポートしているため、開発者にとって非常に柔軟です。

これまで、他の多くの言語と同じように聞こえるすべての特性を見てきたので、他の言語に関してJavaScriptに固有のものを見てみましょう。 いくつかの特徴をリストし、それらが特別な注意を払うに値する理由を説明するために最善を尽くします。

JavaScriptはファーストクラスの関数をサポートします

私はC/C ++のバックグラウンドを持っていたので、JavaScriptを使い始めたばかりのとき、この特性を理解するのは面倒でした。 JavaScriptは関数を第一級市民として扱います。つまり、他の変数と同じように、関数をパラメーターとして他の関数に渡すことができます。

 // we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })

JavaScriptはプロトタイプベースです

他の多くのオブジェクト指向言語の場合と同様に、JavaScriptはオブジェクトをサポートしており、オブジェクトについて考えるときに最初に頭に浮かぶ用語の1つは、クラスと継承です。 この言語はプレーン言語形式のクラスをサポートしておらず、プロトタイプベースまたはインスタンスベースの継承と呼ばれるものを使用しているため、ここで少し注意が必要です。

ES6で正式な用語クラスが導入されたのは今です。つまり、ブラウザーはまだこれをサポートしていません(覚えている場合、執筆時点で、完全にサポートされている最後のECMAScriptバージョンは5.1です)。 ただし、「クラス」という用語がJavaScriptに導入されたとしても、内部ではプロトタイプベースの継承を利用していることに注意することが重要です。

プロトタイプベースのプログラミングは、プロトタイプとして機能する委任を介して既存のオブジェクトを再利用するプロセスを介して動作の再利用(継承と呼ばれる)が実行されるオブジェクト指向プログラミングのスタイルです。 この特性は多くのJavaScriptデザインパターンで使用されているため、記事のデザインパターンのセクションに到達したら、これについてさらに詳しく説明します。

JavaScriptイベントループ

JavaScriptの使用経験がある場合は、コールバック関数という用語に精通しているはずです。 この用語に精通していない人にとって、コールバック関数はパラメーターとして別の関数に送信される関数であり(JavaScriptは関数を第一級市民として扱うことを忘れないでください)、イベントの発生後に実行されます。 これは通常、マウスクリックやキーボードボタンの押下などのイベントをサブスクライブするために使用されます。

JavaScriptイベントループのグラフィック描写

リスナーが接続されているイベントが発生するたびに(そうでない場合、イベントは失われます)、メッセージは、FIFO方式(先入れ先出し)で同期的に処理されているメッセージのキューに送信されます。 )。 これはイベントループと呼ばれます。

キュー上の各メッセージには、関連付けられた機能があります。 メッセージがデキューされると、ランタイムは他のメッセージを処理する前に関数を完全に実行します。 つまり、関数に他の関数呼び出しが含まれている場合、それらはすべて、キューからの新しいメッセージを処理する前に実行されます。 これは、実行から完了までと呼ばれます。

 while (queue.waitForMessage()) { queue.processNextMessage(); }

queue.waitForMessage()は、新しいメッセージを同期的に待機します。 処理中の各メッセージには独自のスタックがあり、スタックが空になるまで処理されます。 完了すると、キューから新しいメッセージが処理されます(存在する場合)。

また、JavaScriptが非ブロッキングであると聞いたことがあるかもしれません。つまり、非同期操作が実行されているとき、プログラムは、非同期操作が完了するのを待っている間、メインをブロックせずに、ユーザー入力の受信などの他のものを処理できます。実行スレッド。 これはJavaScriptの非常に便利なプロパティであり、このトピックだけで記事全体を書くことができます。 ただし、この記事の範囲外です。

デザインパターンとは何ですか?

前に述べたように、デザインパターンは、ソフトウェア設計で一般的に発生する問題に対する再利用可能なソリューションです。 デザインパターンのカテゴリのいくつかを見てみましょう。

プロトパターン

どのようにパターンを作成しますか? 一般的に発生する問題を認識し、この問題に対する独自の解決策があり、それが世界的に認識および文書化されていないとします。 この問題が発生するたびにこのソリューションを使用し、再利用可能であり、開発者コミュニティがその恩恵を受けることができると考えています。

すぐにパターンになりますか? 幸いなことに、違います。 多くの場合、優れたコード記述方法があり、実際にはパターンではないのに、パターンのように見えるものを単に間違えることがあります。

自分が認識していると思うものが実際にデザインパターンであるかどうかをどうやって知ることができますか?

他の開発者の意見を聞いたり、パターン自体を作成するプロセスを知ったり、既存のパターンに精通したりすることによって。 パターンが本格的なパターンになる前に通過しなければならないフェーズがあり、これはプロトパターンと呼ばれます。

プロトパターンは、パターンが有用であることが証明され、正しい結果が得られるさまざまな開発者やシナリオによる一定期間のテストに合格した場合のパターンです。 コミュニティに本格的なパターンを認識させるために行うべき作業とドキュメントは非常に多く、そのほとんどはこの記事の範囲外です。

アンチパターン

デザインパターンは良い習慣を表すので、アンチパターンは悪い習慣を表します。

アンチパターンの例は、 Objectクラスのプロトタイプを変更することです。 JavaScriptのほとんどすべてのオブジェクトはObjectから継承します(JavaScriptはプロトタイプベースの継承を使用することを忘れないでください)。したがって、このプロトタイプを変更したシナリオを想像してみてください。 Objectプロトタイプへの変更は、このプロトタイプから継承するすべてのオブジェクト(ほとんどのJavaScriptオブジェクト)で見られます。 これは起こるのを待っている災害です。

上記の例と同様の別の例は、所有していないオブジェクトを変更することです。 この例は、アプリケーション全体の多くのシナリオで使用されるオブジェクトから関数をオーバーライドすることです。 大規模なチームで作業している場合、これが引き起こす混乱を想像してみてください。 名前の衝突、互換性のない実装、およびメンテナンスの悪夢にすぐに遭遇します。

すべての優れたプラクティスとソリューションについて知ることがどのように役立つかと同様に、悪いものについても知ることは非常に重要です。 このようにして、それらを認識し、前もって間違いを犯さないようにすることができます。

デザインパターンの分類

デザインパターンは複数の方法で分類できますが、最も一般的なものは次のとおりです。

  • 創造的なデザインパターン
  • 構造設計パターン
  • 行動デザインパターン
  • 並行性のデザインパターン
  • 建築デザインパターン

創造的なデザインパターン

これらのパターンは、基本的なアプローチと比較してオブジェクト作成を最適化するオブジェクト作成メカニズムを扱います。 オブジェクト作成の基本的な形式は、設計上の問題や設計の複雑さを増す可能性があります。 作成デザインパターンは、オブジェクトの作成を何らかの方法で制御することにより、この問題を解決します。 このカテゴリで人気のあるデザインパターンのいくつかは次のとおりです。

  • ファクトリメソッド
  • 抽象ファクトリ
  • ビルダー
  • プロトタイプ
  • シングルトン

構造設計パターン

これらのパターンは、オブジェクトの関係を扱います。 これにより、システムの一部が変更された場合でも、システム全体を変更する必要がなくなります。 このカテゴリで最も人気のあるパターンは次のとおりです。

  • アダプタ
  • 複合
  • デコレータ
  • ファサード
  • フライ級
  • プロキシー

行動デザインパターン

これらのタイプのパターンは、システム内の異なるオブジェクト間の通信を認識、実装、および改善します。 これらは、システムの異なる部分が同期された情報を持つことを保証するのに役立ちます。 これらのパターンの一般的な例は次のとおりです。

  • 責任の連鎖
  • 指示
  • イテレータ
  • メディエーター
  • Memento
  • 観察者
  • ストラテジー
  • ビジター

並行性のデザインパターン

これらのタイプのデザインパターンは、マルチスレッドプログラミングパラダイムを扱います。 人気のあるもののいくつかは次のとおりです。

  • アクティブオブジェクト
  • 核反応
  • スケジューラー

建築デザインパターン

建築目的で使用されるデザインパターン。 最も有名なもののいくつかは次のとおりです。

  • MVC(Model-View-Controller)
  • MVP(モデル-ビュー-プレゼンター)
  • MVVM(Model-View-ViewModel)

次のセクションでは、理解を深めるために提供されている例を使用して、前述のデザインパターンのいくつかを詳しく見ていきます。

デザインパターンの例

各デザインパターンは、特定のタイプの問題に対する特定のタイプのソリューションを表しています。 常に最適なパターンの普遍的なセットはありません。 特定のパターンがいつ役立つか、そしてそれが実際の価値を提供するかどうかを学ぶ必要があります。 それらが最適なパターンとシナリオに慣れたら、特定のパターンが特定の問題に適しているかどうかを簡単に判断できます。

特定の問題に間違ったパターンを適用すると、不要なコードの複雑さ、パフォーマンスの不要なオーバーヘッド、さらには新しいアンチパターンの生成などの望ましくない影響が生じる可能性があることを忘れないでください。

これらはすべて、コードにデザインパターンを適用することを検討する際に考慮すべき重要なことです。 私が個人的に有用だと思ったデザインパターンのいくつかを見ていき、すべての上級JavaScript開発者が精通している必要があると信じています。

コンストラクターパターン

古典的なオブジェクト指向言語について考えるとき、コンストラクターはクラス内の特別な関数であり、デフォルト値や送信値のセットを使用してオブジェクトを初期化します。

JavaScriptでオブジェクトを作成する一般的な方法は、次の3つの方法です。

 // either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();

オブジェクトを作成した後、これらのオブジェクトにプロパティを追加する方法は4つあります(ES3以降)。 それらは次のとおりです。

 // supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });

オブジェクトを作成する最も一般的な方法は、中かっこであり、プロパティを追加する場合は、ドット表記または角かっこです。 JavaScriptの経験がある人なら誰でもそれらを使用したことがあります。

JavaScriptはネイティブクラスをサポートしていませんが、関数呼び出しの前に「new」キーワードを使用することでコンストラクターをサポートしていることは前述しました。 このようにして、関数をコンストラクターとして使用し、従来の言語のコンストラクターの場合と同じようにそのプロパティを初期化できます。

 // we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();

ただし、ここにはまだ改善の余地があります。 覚えているかと思いますが、JavaScriptはプロトタイプベースの継承を使用していることを前述しました。 前のアプローチの問題は、 PersonコンストラクターのインスタンスごとにメソッドwritesCodeが再定義されることです。 メソッドを関数プロトタイプに設定することで、これを回避できます。

 // we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();

これで、 Personコンストラクターの両方のインスタンスがwritesCode()メソッドの共有インスタンスにアクセスできるようになりました。

モジュールパターン

特殊性に関する限り、JavaScriptは驚くことをやめません。 JavaScriptに特有のもう1つの点は(少なくともオブジェクト指向言語に関しては)、JavaScriptがアクセス修飾子をサポートしていないことです。 従来のOOP言語では、ユーザーがクラスを定義し、そのメンバーのアクセス権を決定します。 プレーン形式のJavaScriptはクラスもアクセス修飾子もサポートしていないため、JavaScript開発者は、必要に応じてこの動作を模倣する方法を考え出しました。

モジュールパターンの詳細に入る前に、クロージャの概念について説明しましょう。 クロージャは、親関数が閉じられた後でも、親スコープにアクセスできる関数です。 これらは、スコープを通じてアクセス修飾子の動作を模倣するのに役立ちます。 例を使ってこれを示しましょう:

 // we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());

ご覧のとおり、IIFEを使用することにより、カウンター変数を呼び出されて閉じられた関数に関連付けましたが、それをインクリメントする子関数からは引き続きアクセスできます。 関数式の外部からカウンター変数にアクセスできないため、スコープ操作によってプライベート変数にしました。

クロージャを使用して、プライベート部分とパブリック部分を持つオブジェクトを作成できます。 これらはモジュールと呼ばれ、オブジェクトの特定の部分を非表示にして、モジュールのユーザーにのみインターフェイスを公開する場合に非常に役立ちます。 これを例で示しましょう:

 // through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());

このパターンが導入する最も有用なことは、オブジェクトのプライベート部分とパブリック部分の明確な分離です。これは、古典的なオブジェクト指向の背景から来た開発者と非常によく似た概念です。

ただし、すべてが完璧というわけではありません。 メンバーの表示を変更する場合は、パブリック部分とプライベート部分へのアクセスの性質が異なるため、このメンバーを使用した場所でコードを変更する必要があります。 また、作成後にオブジェクトに追加されたメソッドは、オブジェクトのプライベートメンバーにアクセスできません。

モジュールパターンを明らかにする

このパターンは、上記のようにモジュールパターンを改良したものです。 主な違いは、オブジェクトロジック全体をモジュールのプライベートスコープに記述し、匿名オブジェクトを返すことで公開したい部分を単純に公開することです。 プライベートメンバーを対応するパブリックメンバーにマッピングするときに、プライベートメンバーの名前を変更することもできます。

 // we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());

明らかにするモジュールパターンは、モジュールパターンを実装できる少なくとも3つの方法の1つです。 公開モジュールパターンとモジュールパターンの他のバリアントの違いは、主にパブリックメンバーの参照方法にあります。 その結果、明らかになるモジュールパターンは、使用と変更がはるかに簡単になります。 ただし、継承チェーンのプロトタイプとしてRMPオブジェクトを使用するなど、特定のシナリオでは脆弱であることが判明する場合があります。 問題のある状況は次のとおりです。

  1. パブリック関数を参照しているプラ​​イベート関数がある場合、プライベート関数は引き続き関数のプライベート実装を参照し、システムにバグを導入するため、パブリック関数をオーバーライドすることはできません。
  2. プライベート変数を指すパブリックメンバーがあり、モジュールの外部からパブリックメンバーをオーバーライドしようとすると、他の関数は引き続き変数のプライベート値を参照し、システムにバグをもたらします。

シングルトンパターン

シングルトンパターンは、クラスのインスタンスが1つだけ必要なシナリオで使用されます。 たとえば、何かの構成を含むオブジェクトが必要です。 このような場合、システムのどこかで構成オブジェクトが必要になるたびに、新しいオブジェクトを作成する必要はありません。

 var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);

例でわかるように、生成される乱数は常に同じであり、で送信される構成値も同じです。

シングルトン値を取得するためのアクセスポイントは1つだけであり、非常によく知られている必要があることに注意することが重要です。 このパターンを使用することの欠点は、テストがかなり難しいことです。

オブザーバーパターン

オブザーバーパターンは、システムの異なる部分間の通信を最適化された方法で改善する必要があるシナリオがある場合に非常に便利なツールです。 オブジェクト間の緩い結合を促進します。

このパターンにはさまざまなバージョンがありますが、最も基本的な形式では、パターンの2つの主要な部分があります。 1つ目は主題で、2つ目はオブザーバーです。

サブジェクトは、オブザーバーがサブスクライブする特定のトピックに関するすべての操作を処理します。 これらの操作は、オブザーバーを特定のトピックにサブスクライブし、オブザーバーを特定のトピックからサブスクライブ解除し、イベントが公開されたときに特定のトピックについてオブザーバーに通知します。

ただし、このセクションでは例として使用するパブリッシャー/サブスクライバーパターンと呼ばれるこのパターンのバリエーションがあります。 従来のオブザーバーパターンとパブリッシャー/サブスクライバーパターンの主な違いは、パブリッシャー/サブスクライバーがオブザーバーパターンよりもさらに緩い結合を促進することです。

オブザーバーパターンでは、サブジェクトはサブスクライブされたオブザーバーへの参照を保持し、オブジェクト自体から直接メソッドを呼び出しますが、パブリッシャー/サブスクライバーパターンでは、サブスクライバーとパブリッシャーの間の通信ブリッジとして機能するチャネルがあります。 パブリッシャーはイベントを発生させ、そのイベントに対して送信されたコールバック関数を実行するだけです。

パブリッシャー/サブスクライバーパターンの短い例を表示しますが、興味のある人のために、古典的なオブザーバーパターンの例をオンラインで簡単に見つけることができます。

 var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});

このデザインパターンは、発生している1つのイベントに対して複数の操作を実行する必要がある場合に役立ちます。 バックエンドサービスに対して複数のAJAX呼び出しを行い、結果に応じて他のAJAX呼び出しを実行する必要があるシナリオがあるとします。 AJAX呼び出しをもう一方の中にネストする必要があり、コールバック地獄として知られる状況に陥る可能性があります。 パブリッシャー/サブスクライバーパターンを使用することは、はるかに洗練されたソリューションです。

このパターンを使用することの欠点は、システムのさまざまな部分をテストするのが難しいことです。 システムのサブスクライブ部分が期待どおりに動作しているかどうかを知るためのエレガントな方法はありません。

メディエーターパターン

分離されたシステムについて話すときにも非常に役立つパターンについて簡単に説明します。 システムの複数の部分が通信して調整する必要があるシナリオがある場合、おそらく良い解決策はメディエーターを導入することです。

メディエーターは、システムの異なる部分間の通信の中心点として使用され、それらの間のワークフローを処理するオブジェクトです。 ここで、ワークフローを処理することを強調することが重要です。 何でこれが大切ですか?

パブリッシャー/サブスクライバーパターンとの類似性が高いためです。 自問するかもしれませんが、これら2つのパターンはどちらも、オブジェクト間のより良いコミュニケーションを実装するのに役立ちます…違いは何ですか?

違いは、メディエーターがワークフローを処理するのに対し、パブリッシャー/サブスクライバーは「ファイアアンドフォーゲット」タイプの通信と呼ばれるものを使用することです。 パブリッシャー/サブスクライバーは単なるイベントアグリゲーターです。つまり、イベントの発生を処理し、適切なサブスクライバーに発生したイベントを通知するだけです。 イベントアグリゲーターは、イベントが発生した後に何が起こるかを気にしません。これは、メディエーターの場合とは異なります。

メディエーターの良い例は、ウィザードタイプのインターフェースです。 作業したシステムの登録プロセスが大きいとしましょう。 多くの場合、ユーザーから多くの情報が必要な場合は、これを複数のステップに分割することをお勧めします。

このように、コードは非常にクリーンになり(保守が容易になり)、登録を完了するためだけに要求される情報の量にユーザーが圧倒されることはありません。 メディエーターは、各ユーザーが固有の登録プロセスを持つ可能性があるために発生する可能性のあるさまざまなワークフローを考慮して、登録ステップを処理するオブジェクトです。

このデザインパターンの明らかな利点は、システムのさまざまな部分間の通信が改善されたことです。これらの部分はすべて、メディエーターとよりクリーンなコードベースを介して通信します。

欠点は、システムに単一障害点が導入されたことです。つまり、メディエーターに障害が発生すると、システム全体が機能しなくなる可能性があります。

プロトタイプパターン

記事全体ですでに述べたように、JavaScriptはネイティブ形式のクラスをサポートしていません。 オブジェクト間の継承は、プロトタイプベースのプログラミングを使用して実装されます。

これにより、作成中の他のオブジェクトのプロトタイプとして機能できるオブジェクトを作成できます。 プロトタイプオブジェクトは、コンストラクターが作成する各オブジェクトの青写真として使用されます。

これについては前のセクションですでに説明したので、このパターンの使用方法の簡単な例を示しましょう。

 var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();

Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.

コマンドパターン

The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.

This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.

As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.

 // the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));

Facade Pattern

The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.

A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:

 jQuery(".parent .child div.span")

It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.

We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.

次のステップ

Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.

In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.

関連: JS開発者として、これが私を夜更かしするものです/ES6クラスの混乱を理解する