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

本書の翻訳者であるBill Soroは、この言語の登場直後にJavaScriptでコードの最初の行を書いたと語っています。 彼によると、もし彼がいつか彼がJavaScriptのエレガントなデザインパターンに関する一連の記事を発表すると言われたら、彼は笑いで死んでしまうでしょう。 それから彼は、JSを奇妙な小さな言語として認識しました。

しかし、過去20年間で、多くが変化しました。 ダグラス・クロックフォードがJavaScriptで作業していたときに見たように、ビルはJavaScriptを見るようになりました。 強み」:美しく、エレガントで表現力豊かな動的プログラミング言語。



この記事では、Billはしばらくの間喜んで使用してきたすばらしい小さなパターンについて話をしたいと考えています。 彼は、この設計パターンが他のプログラマにも役立つことを望んでいます。 ビルは、自分自身をこのパターンの発見者とは見なしていないと言います。むしろ、他の誰かのコードに似たものを見つけ、それを自分のニーズに合わせたという事実です。

ROROパターン


RORO(オブジェクトを受け取る、オブジェクトを返す-オブジェクトを受け取る、オブジェクトを返す)、これは私の機能のほとんどがobject型の単一のパラメーターを受け入れるパターンであり、それらの多くもobject型の値を返すか、同様の値によって解決されます。

ES2015に登場した破壊機能のせいもあり、ROROは強力で有用なパターンであることに気付きました。 おそらく特別な方法で強調するために、ROROという名前を付けました。

破壊は、現代のJavaScriptの私のお気に入りの機能の1つであるという事実に注目したいと思います。 このメカニズムの長所については、後ほど説明します。そのため、構造化に慣れていない場合は、このビデオを見て、より興味深い内容を読むことができます。

ROROパターンを好む理由は次のとおりです。


これらのRORO機能のそれぞれを詳しく見てみましょう。

名前付きパラメーター


特定のロール(ロール)を持つユーザー(ユーザー)のリストを返す関数があり、連絡先情報(連絡先情報)に各ユーザーを含めるために必要な引数をこの関数に提供する必要があるとします。非アクティブ(非アクティブ)ユーザーの配信に含めるためのもう1つの引数。 従来のアプローチでは、このような関数の宣言は次のようになります。

 function findUsersByRole ( role, withContactInfo, includeInactive ) {...} 

この関数の呼び出しは次のようになります。

 findUsersByRole( 'admin', true, true ) 

コードを読むとき、この呼び出しの最後の2つの引数の役割は完全に理解できないことに注意してください。 2つのtrue値はどういう意味ですか?

アプリケーションがユーザーの連絡先情報をほとんど必要とせず、ほとんどの場合、非アクティブなユーザーのデータを必要とする場合はどうなりますか? ほとんど変わらないにもかかわらず、常に同じ議論に対処する必要があります(これについては後で詳しく説明します)。

一言で言えば、この従来のアプローチは、理解するのが難しく、書くのが難しい不必要な詳細コードでオーバーロードされる可能性がある、理解できない可能性があります。

引数の通常のリストの代わりに、関数の入力で引数を持つ単一のオブジェクトが期待される場合に何が起こるか見てみましょう:

 function findUsersByRole ({ role, withContactInfo, includeInactive }) {...} 

関数宣言は前の例とほとんど同じに見えることに注意してください。唯一の違いは、パラメーターが中括弧に含まれていることです。 これは、関数が3つの個別の引数を受け取る代わりに、プロパティrolewithContactInfo 、およびincludeInactive持つ単一のオブジェクトを期待することを示しています。

このメカニズムは、再構築のおかげで機能します-ES2015に登場した機会です。

これで、次のような関数を呼び出すことができます。

 findUsersByRole({ role: 'admin', withContactInfo: true, includeInactive: true }) 

そのため、コードにはあいまいさがはるかに少なくなり、読みやすくなり、はるかに明確になりました。 さらに、パラメータがオブジェクトの名前付きプロパティによって表されるようになったため、引数をスキップしたり、順序を変更したりしても問題は発生しなくなりました。

たとえば、次のような関数を呼び出すことができます。

 findUsersByRole({ withContactInfo: true, role: 'admin', includeInactive: true }) 

そして、これを行うことができます。

 findUsersByRole({ role: 'admin', includeInactive: true }) 

さらに、新しいパラメーターを追加して、それらの外観が以前に記述されたコードを壊さないようにすることができます。

ここで、引数なしで関数を呼び出す可能性に関連する、つまり関数のすべてのパラメーターがオプションになる可能性に関連する1つの重要な点に注目する価値があります。 同様の関数の呼び出しは次のようになります。

 findUsersByRole() 

引数なしで関数を呼び出すには、引数のデフォルト値を設定する必要があります。

 function findUsersByRole ({ role, withContactInfo, includeInactive } = {}) {...} 

パラメータを持つオブジェクトに対して構造化を使用することの追加の利点は、このアプローチが免疫力を促進することです。 関数に入るときにオブジェクトを破棄するとき、オブジェクトのプロパティを新しい変数に割り当てます。 これらの変数の値を変更しても、元のオブジェクトは変更されません。

次の例を考えてみましょう。

 const options = { role: 'Admin', includeInactive: true } findUsersByRole(options) function findUsersByRole ({ role, withContactInfo, includeInactive } = {}) { role = role.toLowerCase() console.log(role) // 'admin' ... } console.log(options.role) // 'Admin' 

ここでは、 role変数の値を変更したという事実にもかかわらず、 options.roleオブジェクトのプロパティの値は変更されませんでした。

引数を持つオブジェクトのプロパティのいずれかがオブジェクト(たとえば、 arrayまたはobjectのタイプ)である場合、その結果、構造化によりオブジェクトの浅いコピーが作成されることに注意してください。その後、このプロパティを変更すると、別の変数に割り当てられても、元のオブジェクトが変更されます これに注意を引いてくれたYuri Khomyakovに感謝します。

デフォルトのパラメーター値


ES2015の革新により、JS関数を宣言するときに、パラメーターのデフォルト値を設定できるようになりました。 実際、オブジェクトをパラメーターで記述した後、宣言に={}を追加することで、デフォルトパラメーターの使用を既にここで示しました。

従来のパラメーターを使用する場合、デフォルトのパラメーター値を追加すると次のようになります。

 function findUsersByRole ( role, withContactInfo = true, includeInactive ) {...} 

includeInactiveパラメーターをtrueに設定する必要がある場合、デフォルト値を保持するためにwithContactInfoの値としてundefinedを明示的に渡す必要があります。

 findUsersByRole( 'Admin', undefined, true ) 

これはすべて非常に神秘的に見えます。

これを、関数宣言のパラメーターを持つオブジェクトの使用と比較してください。

 function findUsersByRole ({ role, withContactInfo = true, includeInactive } = {}) {...} 

この関数は次のように呼び出すことができます:

 findUsersByRole({ role: 'Admin', includeInactive: true }) 

同時に、 withContactInfo設定されたデフォルト値は消えません。

必要な関数パラメーターについて


ここでは、適切な操作に絶対に必要な関数パラメーターを宣言するときの指示に関する1つの有用なトリックを検討するために、メイントピックから少し逸脱します。

以下のコードに似た何かを頻繁に書く必要がありますか?

 function findUsersByRole ({ role, withContactInfo, includeInactive } = {}) { if (role == null) {    throw Error(...) } ... } 

ここでは、二重の等号==を使用して、 nullundefined両方の値を確認します。

このすべての代わりに、デフォルトのパラメーターを使用できることを伝えた場合、関数内の必要なデータの受信の検証が実装されるため、どうなりますか?

このようなチェックを実行するには、まずrequiredParam()関数を宣言するrequiredParam()ありますが、これはエラーを返します。

 function requiredParam (param) { const requiredParamError = new Error(  `Required parameter, "${param}" is missing.` ) //   - if (typeof Error.captureStackTrace === 'function') {   Error.captureStackTrace(     requiredParamError,     requiredParam   ) } throw requiredParamError } 

ちなみに、この関数はROROを使用しないことを知っていますが、そのため最初の段階では、すべての関数がこのパターンを使用するとは言いませんでした。

これで、 requiredParam関数の呼び出しをroleデフォルト値として設定できます。

 function findUsersByRole ({ role = requiredParam('role'), withContactInfo, includeInactive } = {}) {...} 

その結果、誰かがroleを指定せずにfindUserByRole関数を呼び出すと、必要なroleパラメーターを関数に渡すのを忘れたことを示すエラーメッセージが表示されRequired parameter, "role" is missingます。

技術的な観点から、通常のデフォルトパラメータでこのアプローチを使用できます。 ここでのオブジェクトは任意です。 ただし、これは非常に便利なため、ここで説明しました。

複雑な値のセットの関数から戻る


JavaScriptの関数は1つの値のみを返すことができます。 この値がobject型のobject 、多くの興味深いものを含めることができます。

データベースにUserオブジェクトを保存する関数を想像してください。 この関数がオブジェクトを返す場合、このアプローチを使用しない場合よりも、呼び出し元の場所にはるかに多くの情報を渡すことができます。

たとえば、通常、対応する関数でデータベースに情報を保存するときに、新しい行をテーブルに挿入するアプローチが使用されます。新しい行がまだ存在しない場合、対応する行が存在する場合、更新されます。

そのような場合、データストレージ関数が実行したデータベース操作の種類INSERTまたはUPDATEを知ることは有用です。 データベースに保存されたものの正確な説明を取得することは良いことであり、操作の結果に関する詳細は害になりません。 たとえば、正常に完了したり、より大きなトランザクションと組み合わせてキューに入れたりすることができます。操作のタイムアウトが期限切れになったため、書き込みの試行が失敗する場合があります。

関数からオブジェクトを返すと、必要な情報をすべて簡単に入力できます。

 async saveUser({ upsert = true, transaction, ...userInfo }) { //   return {   operation, //  'INSERT'   status, //  'Success'   saved: userInfo } } 

技術的には、次の構造はPromiseオブジェクトを返します。これは、タイプobject通常のオブジェクトによって解決されobject 、この例はここで考えられているアイデアをよく示していると思います。

機能の構成を簡素化する


関数構成は、2つ以上の関数を組み合わせて新しい関数を作成するプロセスです。 関数の構成は、データの受け渡しが計画されているいくつかのパイプのシステムを組み立てることに似ています。
エリック・エリオット

関数の構成は、次のような宣言の特別なpipe関数を使用して実行できます。

 function pipe(...fns) { return param => fns.reduce(   (result, fn) => fn(result),   param ) } 

この関数は、関数のリストを取得し、これらの関数を左から右に実行できる1つの関数を返します。これらの関数の最初の関数をpipeに渡される引数に渡し、2番目の関数を最初の関数に返します。

おそらくこれは少しわかりにくいので、今度はすべてをその場所に配置する例を見てみましょう。 このアプローチの唯一の制限は、リスト内の各関数がパラメーターを1つだけ受け取ることです。 幸い、ROROパターンを使用する場合、これは問題ではありません。

以下に例を示します。 関数saveUserがあります。これは、データをチェック、正規化、保存する3つの個別の関数を介してuserInfoオブジェクトをuserInfoます。

 function saveUser(userInfo) { return pipe(   validate,   normalize,   persist )(userInfo) } 

validatenormalize 、およびpersist関数でいわゆるrestパラメーターを使用して、各関数に必要な値のみを破棄し、他のすべてを渡します。

わかりやすくするための小さなコードを次に示します。

 function validate( id, firstName, lastName, email = requiredParam(), username = requiredParam(), pass = requiredParam(), address, ...rest ) { //  -  return {   id,   firstName,   lastName,   email,   username,   pass,   address,   ...rest } } function normalize( email, username, ...rest ) { //   return {   email,   username,   ...rest } } async function persist({ upsert = true, ...info }) { //  userInfo    return {   operation,   status,   saved: info } } 

ROか否か-それが問題です


最初に、ほとんどの関数がオブジェクトを受け入れ、それらの多くもオブジェクトを返すという事実について話しました。 多くではあるが、すべてではない。 他のパターンと同様に、ROROは開発者の武器のツールの1つとしてのみ考慮されるべきです。 便利な場所で使用し、関数パラメーターのリストをより理解しやすく柔軟にし、関数によって返される値をより表現力豊かにします。

1つの引数を渡すだけで十分な関数を作成する場合、オブジェクトを渡すことはすでに多すぎます。 同様に、必要なものをすべて明確に表現する呼び出しの場所に単純な値を渡すことができる関数を作成する場合、そのような関数はobject型の値をまったく返さないことが明らかです。

たとえば、請求確認機能でROROを使用することはほとんどありません。 渡された値が負でない整数かどうかをチェックするisPositiveInteger関数があるとします。 このような関数を作成する場合、ROROパターンには意味がありません。 ただし、JavaScriptアプリケーションを開発する場合、このパターンは他の多くの状況で役立ちます。

親愛なる読者! コードでROROパターンを使用する予定はありますか? または、あなたはすでに類似のものを使用していますか?

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


All Articles