最新のJavaScriptのエレガントなパターン:Ice Factory

JavaScriptのデザインパターン専用の次のBill Soroマテリアルの翻訳に注目してください。 前回 ROROパターンについて話しましたが、今日のテーマはIce Factoryパターンです。 簡単に言えば、このテンプレートは「凍結」オブジェクトを返す関数です。 これは非常に重要で強力なパターンであり、解決することを目的としたJS問題の1つについて説明していきます。



画像



JavaScriptクラスの問題



多くの場合、関連する機能を単一のオブジェクトにグループ化する価値があります。 たとえば、オンラインストアアプリケーションには、パブリックメソッドaddProduct



およびremoveProduct



を含むcart



オブジェクトがある場合があります。 これらのメソッドは、 cart.addProduct()



およびcart.removeProduct()



コンストラクトを使用してcart.addProduct()



ことができます。



JavaやC#などの言語からJavaScript開発に来た場合、クラスが最前線にあり、すべてがオブジェクトに焦点を合わせている場合、上記の状況はおそらく完全に自然なものとして認識されます。



プログラミングを習い始めたばかりであれば、今はcart.addProduct()



という形式の式に精通しています。 そして、単一のオブジェクトのメソッドによって表される関数をグループ化するという考え方も、あなたにとって明らかだと思います。



cart



オブジェクトの作成方法について説明しましょう。 おそらく、現代のJavaScriptの機能を考えると、最初に思い浮かぶのは、 class



キーワードにアクセスすることです。 たとえば、次のようになります。



 // ShoppingCart.js export default class ShoppingCart { constructor({db}) {   this.db = db } addProduct (product) {   this.db.push(product) } empty () {   this.db = [] } get products () {   return Object     .freeze(this.db) } removeProduct (id) {   //   } //   } // someOtherModule.js const db = [] const cart = new ShoppingCart({db}) cart.addProduct({ name: 'foo', price: 9.99 })
      
      





db



パラメータとして配列を使用していることに注意してください。 これは、例を単純化するために行われます。 実際のコードでは、そのような変数は、実際のデータベースとやり取りするModelまたはRepoオブジェクトのようなもので表されます。



残念ながら、これはすべて見栄えは良いものの、JavaScriptのクラスの動作は予想とはまったく異なります。 比ur的に言えば、JSでクラスを操作するときに注意を怠ると、噛み付かれる可能性があります。



たとえば、 new



キーワードを使用して作成されたオブジェクトは変更可能です。 これは、たとえば、メソッドをオーバーライドできることを意味します。



 const db = [] const cart = new ShoppingCart({db}) cart.addProduct = () => 'nope!' //     ! cart.addProduct({ name: 'foo', price: 9.99 }) // : "nope!" ?
      
      





実際、 new



キーワードを使用して作成されたオブジェクトは、それらの作成に使用されたクラスのプロトタイプを継承するため、さらに悪化します。 したがって、このクラスのプロトタイプの変更は、このクラスに基づいて作成されたすべてのオブジェクトに影響し、これらの変更がオブジェクトの作成後に行われた場合でも同様です。



以下に例を示します。



 const cart = new ShoppingCart({db: []}) const other = new ShoppingCart({db: []}) ShoppingCart.prototype .addProduct = () => 'nope!' //     ! cart.addProduct({ name: 'foo', price: 9.99 }) // : "nope!" other.addProduct({ name: 'bar', price: 8.88 }) // : "nope!"
      
      





次に、JavaScriptのthis



動的バインディングを覚えておいてください。 cart



オブジェクトのメソッドをどこかに渡すと、 this



への元の参照が失われる可能性があります。 この動作は直感的ではなく、多くの問題の原因になる可能性があります。



this



がプログラマに適している通常の迷惑は、オブジェクトメソッドがイベントハンドラに割り当てられている場合です。 バスケットを空にするためのcart.empty



メソッドを考えます:



 empty () {   this.db = [] }
      
      





このメソッドをWebページ上の特定のボタンのclick



イベントハンドラーに割り当てます。



 <button id="empty"> Empty cart </button> --- document .querySelector('#empty') .addEventListener(   'click',   cart.empty )
      
      





ユーザーがこのボタンをクリックしても、何も変わりません。 cart



オブジェクトで表されるバスケットはいっぱいのままです。



同時に、これはすべてエラーメッセージなしで発生します。 this



は、バスケットではなくボタンを使用する必要があるためです。 その結果、 cart.empty



を呼び出すと、 cart



オブジェクトのdb



プロパティに影響を与えるのではなく、 db



という名前のボタンの新しいプロパティが作成され、このプロパティに空の配列が割り当てられます。



このエラーは、開発者を文字通り狂わせることができるもののカテゴリに属します。これは、一方ではエラーメッセージがなく、他方では常識の観点からは非常に機能し、実際にはそのように動作しないコード予想通り。



上記のコードに期待どおりの処理を強制するには、次のようにする必要があります。



 document .querySelector("#empty") .addEventListener(   "click",   () => cart.empty() )
      
      





または:



 document .querySelector("#empty") .addEventListener(   "click",   cart.empty.bind(cart) )
      
      





このビデオでは、これらすべての優れた説明を見つけることができると思いますが、このビデオからの引用は次のとおりです。 «new



JavaScriptの«new



ものは、非論理的で、奇妙で、神秘的なtrapです」



JSクラスの問題の解決策としてのIce Factoryパターン



既に述べたように、Ice Factoryパターンは、「凍結」オブジェクトを作成して返す関数です。 このデザインパターンを使用する場合、ショッピングカートの例は次のようになります。



 // makeShoppingCart.js export default function makeShoppingCart({ db }) { return Object.freeze({   addProduct,   empty,   getProducts,   removeProduct,   //  }) function addProduct (product) {   db.push(product) } function empty () {   db = [] } function getProducts () {   return Object     .freeze(db) } function removeProduct (id) {   //   } //   } // someOtherModule.js const db = [] const cart = makeShoppingCart({ db }) cart.addProduct({ name: 'foo', price: 9.99 })
      
      





「奇妙で神秘的なtrap」が消えたことに注意してください。 つまり、このコードを分析して、次の結論を導き出すことができます。





プライベートプロパティとメソッド



Ice Factoryテンプレートのもう1つの利点は、テンプレートで作成されたオブジェクトにプライベートプロパティとメソッドを含めることができることです。 例を考えてみましょう:



 function makeThing(spec) { const secret = 'shhh!' return Object.freeze({   doStuff }) function doStuff () {   //      spec,   //  secret } } //  secret  const thing = makeThing() thing.secret // undefined
      
      





これは、クロージャーメカニズムのおかげで可能です 。クロージャーメカニズムの詳細については、 こちらを参照してください



Ice Factoryパターンの起源について



Factory Functionsは常にJavaScriptに含まれていますが、Ice Factoryパターンを設計するためにダグラスクロックフォードこのビデオで示したコードに真剣に触発されました。 これは、彼が「コンストラクター」と呼ぶ関数を使用してオブジェクトの作成を実証するショットです。









ダグラス・クロックフォードは私にインスピレーションを与えたコードを示しています



Crockfordが示したもののバリエーションである私のバージョンのコードは、次のようになります。



 function makeSomething({ member }) { const { other } = makeSomethingElse() return Object.freeze({   other,   method }) function method () {   // ,   "member" } }
      
      





「上昇」関数を利用して、リターン式をコードの上部に近づけました。 その結果、詳細を掘り下げる前に、このコードをすぐに読む人は、何が起こっているのかについての全体像を見ることができます。



さらに、 spec



パラメーターを破棄するために使用しました。 また、このテンプレートに名前を付けて、Ice Factoryという名前を付けました。 覚えやすく、JSクラスのconstructor



関数と混同しにくいと思います。 しかし、一般的に、私のパターンとコンストラクターはまったく同じです。

したがって、このパターンの作成者について話している場合、それはダグラス・クロックフォードに属します。



Crockfordは機能強化をJavaScriptの「弱点」と見なしているため、おそらく私のアプローチは気に入らないでしょう。 これに対する私の態度については、 以前の記事の 1つ、特にこの解説で説明しました。



継承と製氷工場



オンラインストア用のアプリケーションの作成について考え続けると、バスケットを操作するときに製品を追加および削除するという概念がさまざまな場所に何度も現れることがすぐに理解できます。



買い物かごに加えて、おそらくオブジェクトカタログ( Catalog



)と注文( Order



)があります。 同時に、これらのオブジェクトには、多くの場合、開いているaddProduct



removeProduct



removeProduct



バリアントがありremoveProduct







コードの複製が悪いことはわかっているため、最終的にはバスケット、カタログ、注文オブジェクトを継承する製品のリストであるproductList



オブジェクトのようなものを作成することになります。



Ice Factoryテンプレートを使用して作成されたオブジェクトは展開できないため、他のオブジェクトから継承することはできません。 これを考えて、コードの複製をどうしますか? 商品のリストであるオブジェクトを使用することで、いくつかのメリットを得ることができますか?



もちろんできます!



Ice Factoryパターンは、最も影響力のあるプログラミングの本の1つである「オブジェクト指向プログラミングテクニック」で引用されている永遠の原則を適用するように導きます。 デザインパターン。」 この原則:「クラス継承よりも構成を優先する」。



Gang of Fourとして知られるこの本の著者は、次のように述べています。「しかし、私たちの経験は、デザイナーが継承を乱用していることを示しています。 多くの場合、作成者がオブジェクトの構成にもっと依存すれば、設計はより良く簡単になります。」



だから、ここに製品のリストがあります:



 function makeProductList({ productDb }) { return Object.freeze({   addProduct,   empty,   getProducts,   removeProduct,   //  )} //   // addProduct   ... }   : function makeShoppingCart({  addProduct,  empty,  getProducts,  removeProduct,  //  }) {   return Object.freeze({     addProduct,     empty,     getProducts,     removeProduct,     someOtherMethod,    //    )} function someOtherMethod () {   //  } }
      
      





これで、製品のリストを表すオブジェクトをバスケットを表すオブジェクトに簡単に埋め込むことができます。



 const productDb = [] const productList = makeProductList({ productDb }) const cart = makeShoppingCart(productList)
      
      





まとめ



何か新しいことを学ぶとき、特にアプリケーションを設計するためのアーキテクチャーアプローチなど、かなり複雑なことになると、私たちは学ぶことに関する明確なルールを期待する傾向があります。 私たちは次のようなことを聞​​きたいと思っています:「常にこれを行い、これを決してしない」。



ソフトウェア開発環境でのローテーションが長ければ長いほど、「常に」「決して」という概念はないことをよく理解できます。 プログラマーは常に、特定の状況に適用される特定の手法の長所と短所の組み合わせによって決定される選択肢を持っています。



上記では、Ice Factoryパターンの長所について説明しました。 しかし、彼には欠点もあります。 それらは、このパターンを使用するオブジェクトの作成がクラスを使用するよりも遅く、より多くのメモリを必要とするという事実にあります。



上記のこのパターンの使用では、これらのマイナスは問題になりません。 特に、Ice Factoryはクラスを使用するよりも遅くなりますが、このパターンは非常に高速に動作します。



いわば何十万ものオブジェクトを作成する必要がある場合、またはパフォーマンスとメモリ消費が最前線にある状況にある場合は、通常のクラスの方がおそらく適しています。



主なもの-アプリケーションのプロファイルを作成することを忘れないでください。また、時期尚早な最適化に努めません。 オブジェクトの作成がプログラムのボトルネックになることはめったにありません。



私が上で言ったという事実にもかかわらず、JSクラスの機能は常に不利と見なされるとは限りません。 たとえば、クラスがそこで使用されているという理由だけでライブラリまたはフレームワークを拒否するべきではありません。 これは、このテーマに関するダン・アブラモフの優れた資料です。



そして最後に、上で引用したコードスニペットでは、絶対的な真実のようなものではなく、自分の好みだけで決まる多くのアーキテクチャソリューションを使用したことを認めなければなりません。 それらのいくつかを次に示します。





コードスタイルには他のアプローチを使用できますが、これは完全に正常です。 スタイルはパターンではありません。



一般に、Ice Factoryの設計パターンは、凍結オブジェクトを作成して返す関数を使用することに要約されます。 そして、そのような関数をどのように正確に書くかは、自分で決めることができます。



親愛なる読者! プロジェクトでIce Factoryパターンのようなものを使用していますか?






All Articles