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」が消えたことに注意してください。 つまり、このコードを分析して、次の結論を導き出すことができます。
-
new
キーワードは不要になりnew
。 ここで、cart
オブジェクトを作成するには、通常のJS関数を呼び出します。
-
this
不要になりました。 これで、オブジェクトのメソッドから直接db
アクセスできます。
- 現在、
cart
オブジェクトは不変です。Object.freeze()
コマンドはそれをフリーズします。 これにより、新しいプロパティを追加したり、既存のプロパティを削除または変更することはできません。 また、プロトタイプも変更できません。 ここでは、Object.freeze()
コマンドがオブジェクトのいわゆる「小さなフリーズ」を実行することだけを覚えておく価値があります。 つまり、返されたオブジェクトに配列または別のオブジェクトが含まれている場合、Object.freeze()
コマンドもそれらに適用する必要があります。 さらに、凍結されたオブジェクトがESモジュールの外部で使用される場合、オブジェクトに変更を加えようとしたときにエラーが生成されることを保証するために、 厳格モードを使用する必要があります。
プライベートプロパティとメソッド
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クラスの機能は常に不利と見なされるとは限りません。 たとえば、クラスがそこで使用されているという理由だけでライブラリまたはフレームワークを拒否するべきではありません。 これは、このテーマに関するダン・アブラモフの優れた資料です。
そして最後に、上で引用したコードスニペットでは、絶対的な真実のようなものではなく、自分の好みだけで決まる多くのアーキテクチャソリューションを使用したことを認めなければなりません。 それらのいくつかを次に示します。
- Function Expressionの代わりにFunction Statementコンストラクトを使用します。
- returnコマンドをコードの先頭近くに配置しました(これはFunction Statementコンストラクトを使用することで可能になります)。
-
createX
命名createX
、createX
を使用するのではなく、makeX
パターンでファクトリー関数を呼び出します。
- 私のファクトリー関数は、 単一の非構造化パラメーターをオブジェクトとして受け入れます 。
- 私はセミコロンを使用しません( Crockfordは間違いなくこれを承認しません )。
コードスタイルには他のアプローチを使用できますが、これは完全に正常です。 スタイルはパターンではありません。
一般に、Ice Factoryの設計パターンは、凍結オブジェクトを作成して返す関数を使用することに要約されます。 そして、そのような関数をどのように正確に書くかは、自分で決めることができます。
親愛なる読者! プロジェクトでIce Factoryパターンのようなものを使用していますか?