JavaScriptのクラスとファクトリー関数。 何を選択しますか?

JavaScriptには、オブジェクトを作成するさまざまな方法があります。 特に、 classキーワードといわゆるファクトリー関数を使用する構成について話します。 本書の翻訳者である著者は、これらの2つの概念を探求し、それぞれの長所と短所に関する質問への回答を探して比較します。

画像

復習


classキーワードはECMAScript 2015(ES6)に登場しました。その結果、オブジェクトを作成するための2つの競合するパターンができました。 それらを比較するために、クラス構文を使用してファクトリー関数を適用し、同じオブジェクト( TodoModel )を説明します。

classキーワードを使用すると、 TodoModel 説明は次のようになります。

 class TodoModel {   constructor(){       this.todos = [];       this.lastChange = null;   }     addToPrivateList(){      console.log("addToPrivateList");   }   add() { console.log("add"); }   reload(){} } 

ファクトリー関数によって作成された同じオブジェクトの説明は次のとおりです。

 function TodoModel(){   var todos = [];   var lastChange = null;         function addToPrivateList(){       console.log("addToPrivateList");   }   function add() { console.log("add"); }   function reload(){}     return Object.freeze({       add,       reload   }); } 

クラスを作成するこれらの2つのアプローチの機能を検討してください。

カプセル化


クラスとファクトリー関数を比較するときに見られる最初の機能は、 classキーワードを使用して作成されたオブジェクトのすべてのメンバー、フィールド、およびメソッドが公開されていることです。

 var todoModel = new TodoModel(); console.log(todoModel.todos);     //[] console.log(todoModel.lastChange) //null todoModel.addToPrivateList();     //addToPrivateList 

ファクトリー関数を使用する場合、意識的に開いているもののみが公開され、他のすべては受信したオブジェクト内に隠されます。

 var todoModel = TodoModel(); console.log(todoModel.todos);     //undefined console.log(todoModel.lastChange) //undefined todoModel.addToPrivateList();     //taskModel.addToPrivateList                                   is not a function 

API耐性


オブジェクトが作成された後、そのAPIが変更されないこと、つまり、それに対する耐性が期待されます。 ただし、 classキーワードを使用して作成されたオブジェクトのパブリックメソッドの実装を簡単に変更できます。

 todoModel.reload = function() { console.log("a new reload"); } todoModel.reload();            //a new reload 

この問題は、クラスの宣言後にObject.freeze(TodoModel.prototype)呼び出すか、サポートされている場合にデコレーターを使用してクラスを「フリーズ」することで解決できます。

一方、ファクトリ関数を使用して作成されたオブジェクトのAPIは不変です。 Object.freeze()コマンドを使用して、新しいオブジェクトのパブリックメソッドのみを含む戻りオブジェクトを処理することに注意してください。 このオブジェクトのプライベートデータは変更できますが、これはこれらのパブリックメソッドを使用してのみ実行できます。

 todoModel.reload = function() { console.log("a new reload"); } todoModel.reload();            //reload 

このキーワード


classキーワードで作成されたオブジェクトは、 thisコンテキストを失うという長年の問題を起こしやすい傾向があります。 たとえば、ネストされた関数ではコンテキストが失われます。 これはプログラミングプロセスを複雑にするだけでなく、このような動作はエラーの恒常的な原因でもあります。

 class TodoModel {   constructor(){       this.todos = [];   }     reload(){       setTimeout(function log() {          console.log(this.todos);    //undefined       }, 0);   } } todoModel.reload();                   //undefined 

そして、DOMイベントで適切なメソッドを使用すると、コンテキストが失われます。

 $("#btn").click(todoModel.reload);    //undefined 

ファクトリー関数を使用して作成されたオブジェクトは、thisキーワードがここでは使用されないため、同様の問題の影響を受けません。

 function TodoModel(){   var todos = [];         function reload(){       setTimeout(function log() {          console.log(todos);        //[]      }, 0);   } } todoModel.reload();                   //[] $("#btn").click(todoModel.reload);    //[] 

このキーワードと矢印関数


矢印関数は、クラスを使用するときにthisコンテキストが失われることに関連する問題を部分的に解決しますが、同時に新しい問題を作成します。 つまり、クラスで矢印関数を使用する場合、 thisはネストされた関数のコンテキストを失わなくなりました。 ただし、 thisはDOMイベントを処理するときにコンテキストを失います。

矢印関数を使用してTodoModelクラスを再設計しました。 リファクタリングの過程で、通常の関数を矢印関数に置き換えると、コードの可読性にとって重要なもの、つまり関数名が失われることに注意してください。 次の例を見てください。

 //      setTimeout(function renderTodosForReview() {     /* code */ }, 0); //       setTimeout(() => {     /* code */ }, 0); 

矢印関数を使用する場合、関数の機能を正確に理解するには、関数のテキストを読む必要があります。 すべてのコードを読むのではなく、関数の名前を読んでその本質を理解したいと思います。 もちろん、矢印関数を使用すると、コードを読みやすくすることができます。 たとえば、次のような矢印関数を使用する習慣を作ることができます。

 var renderTodosForReview = () => {    /* code */ }; setTimeout(renderTodosForReview, 0); 

新しいオペレーター


クラスに基づいてオブジェクトを作成する場合、 new演算子を使用する必要があります。 また、ファクトリ関数を使用してオブジェクトを作成する場合、 new必要newません。 ただし、 new使用するとコードの可読性が向上する場合、この演算子はファクトリー関数でも使用できますが、これによる害はありません。

 var todoModel= new TodoModel(); 

ファクトリ関数でnew使用する場合、関数は作成したオブジェクトを単に返します。

安全性


アプリケーションがUserオブジェクトを使用して認証メカニズムを操作するとします。 ここで説明した両方のアプローチを使用して、このようなオブジェクトをいくつか作成しました。

クラスを使用したUserオブジェクトの説明は次のとおりです。

 class User {   constructor(){       this.authorized = false;   }     isAuthorized(){       return this.authorized;   } } const user = new User(); 

ファクトリー関数を使用して記述された同じオブジェクト次のようになります。

 function User() {   var authorized = false;        function isAuthorized(){      return authorized;   }     return Object.freeze({       isAuthorized   }); } const user = User(); 

classキーワードを使用して作成されたオブジェクトは、攻撃者がオブジェクトへのリンクを持っている場合、攻撃に対して脆弱です。 すべてのオブジェクトのすべてのプロパティは公開されているため、攻撃者は他のオブジェクトを使用して、関心のあるオブジェクトにアクセスできます。

たとえば、 user変数がグローバルの場合、開発者コンソールから適切な権限を直接取得できます。 これを確認するには、 サンプルコードを開き、コンソールからuser変数を変更しuser

この例はPlunkerリソースを使用して準備されました。 グローバル変数にアクセスするには、コンソールタブのコンテキストをtopからplunkerPreviewTarget(run.plnkr.co/)ます。

 user.authorized = true;            //    user.isAuthorized = function() { return true; }  // API console.log(user.isAuthorized());  //true 


開発者コンソールを使用してオブジェクトを変更する

ファクトリー関数を使用して作成されたオブジェクトは、外部から変更できません。

構成と継承


クラスは、オブジェクトの継承と構成の両方をサポートします。

SpecialServiceクラスがServiceクラスの継承である継承のを作成しまし

 class Service {   log(){} } class SpecialService extends Service {  logSomething(){ console.log("logSomething"); } } var specialService = new SpecialService(); specialService.log(); specialService.logSomething(); 

ファクトリ関数を使用する場合、継承はサポートされていません;ここでは、構成のみを使用できます。 または、 Object.assign()コマンドを使用して、既存のオブジェクトからすべてのプロパティをコピーできます。 たとえばSpecialServiceオブジェクトでServiceオブジェクトのすべてのメンバーを再利用する必要があるとします。

 function Service() {   function log(){}          return Object.freeze({       log   }); } function SpecialService(args){  var standardService = args.standardService;  function logSomething(){      console.log("logSomething");  }  return Object.freeze(Object.assign({}, standardService, {      logSomething  })); } var specialService = SpecialService({      standardService : Service()   }); specialService.log(); specialService.logSomething(); 

ファクトリー関数は、継承の代わりに構成の使用を容易にします。これにより、開発者はアプリケーション設計に関してより高いレベルの柔軟性を得ることができます。

クラスを使用する場合、継承よりも合成を優先することもできます。実際、これらは既存の動作の再利用に関するアーキテクチャ上の決定にすぎません。

記憶


クラスを使用すると、プロトタイプシステムに基づいて実装されるため、メモリを節約できます。 すべてのメソッドはプロトタイプで一度だけ作成され、クラスのすべてのインスタンスで使用されます。

ファクトリ関数を使用して作成されたオブジェクトによって消費されるメモリの追加コストは、数千の同様のオブジェクトが作成された場合にのみ顕著です。

ファクトリー機能を使用するための典型的なメモリコストを見つけるために使用されるページは次のとおりです。 以下は、10個と20個のメソッドを使用して、異なる数のオブジェクトについてChromeで得られた結果です。


メモリオーバーヘッド(Chromeで)

OOPオブジェクトとデータ構造


メモリコストの分析を続ける前に、2種類のオブジェクトを区別する必要があります。


オブジェクトは動作を提供し、データを隠します。

データ構造はデータを提供しますが、重要な動作はありません。

ロバートマーティン、 クリーンコード

オブジェクトとデータ構造の違いを明確にするために、すでにTodoModelいるTodoModelオブジェクトの例を見てください。

 function TodoModel(){   var todos = [];            function add() { }   function reload(){ }        return Object.freeze({       add,       reload   }); } 

TodoModelオブジェクトTodoModeltodoオブジェクトのリストを保存および管理します。 TodoModelはOOPオブジェクトであり、動作を提供し、データを隠します。 アプリケーションにはインスタンスが1つしかありません。そのため、ファクトリ関数を使用して作成する場合、追加のメモリコストは必要ありません。

todos配列に格納されているオブジェクトはデータ構造です。 プログラムにはそのようなオブジェクトが多数存在する場合がありますが、これらは通常のJavaScriptオブジェクトです。 それらのメソッドをプライベートにすることに興味はありません。 むしろ、すべてのプロパティとメソッドが公開されるように努めています。 その結果、これらすべてのオブジェクトはプロトタイプシステムを使用して構築されるため、メモリを節約できます。 通常のオブジェクトリテラルObject.create()Object.create()コマンドObject.create()作成できます。

ユーザーインターフェイスコンポーネント


アプリケーションは、数百または数千のユーザーインターフェイスコンポーネントのインスタンスを持つことができます。 これは、カプセル化とメモリの節約の間で妥協点を見つけなければならない状況です。

コンポーネントは、使用されているフレームワークで採用されている方法に従って作成されます。 たとえば、Vueはオブジェクトリテラルを使用し、Reactはクラスを使用します。 コンポーネントオブジェクトの各メンバーは公開されますが、プロトタイプシステムの使用のおかげで、そのようなオブジェクトを使用するとメモリを節約できます。

反対のOOPパラダイム


広い意味で、クラスとファクトリー関数は、オブジェクト指向プログラミングの2つの相反するパラダイムの戦いを示しています。

JavaScriptに適用されるクラスベースのOOPは、次のことを意味します。


クラスを使用しないOOPは次のよ​​うになります。


まとめ


クラスの長所は、開発がクラスに基づいている言語からJSに来るプログラマーに馴染みがあることです。 JSのクラスは、プロトタイプシステムの構文糖衣です。 ただし、セキュリティの問題と、コンテキストの喪失による永続的なエラーにつながるthisの使用により、ファクトリー関数と比較してクラスが2番目になります。 例外として、クラスは、Reactなどで使用されるフレームワークで使用される場合に再分類されます。

ファクトリ関数は、安全でカプセル化された柔軟なOOPオブジェクトを作成するためのツールだけではありません。 クラスを作成するこのアプローチは、JavaScriptに固有の新しいプログラミングパラダイムへの扉も開きます。

この記事の終わりに、 Douglas Crockfordを引用できます。「クラスのないOOPはJavaScriptからの人類への贈り物だと思います。」

親愛なる読者! クラスとファクトリー関数のどちらに近いのですか?

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


All Articles