アプリケーション状態の過度の複雑さを回避する方法[翻訳]





ReduxなどのFlux実装は、アプリケーションの状態の設計により注意を払うよう動機付けます。 これは簡単な作業ではないことがわかりました。 これは、一見無害な蝶の羽の羽ばたきが広範囲にわたる結果につながる、カオスの理論からの古典的な例に似ています。 以下は、アプリケーションの状態をよりよく整理するためのヒントです。



アプリケーションの状態とは何ですか?



ウィキペディアによると、プログラムはコンピューターのメモリに表示される変数にデータを保存します。 特定の時点でのこれらの変数の値は、アプリケーションの状態です。



定義に「最小」という言葉を追加することが重要です。 アプリケーションの状態を設計するときは、最小限のデータセットで作業し、それに基づいて計算できる変数を無視する必要があります。



単方向データストリーム( Flux )アプリケーションでは、状態はストア内にあります。 アクションの実行により状態が変化し、その結果、その変更にサブスクライブされているビューが新しいデータに従って更新されます。







後で説明するReduxは 、いくつかの厳しい制限を追加します。 まず、状態は単一の不変でシリアライズ可能なストレージに保存されます。



以下は、Reduxを使用していない場合でも役立つヒントです。 これらは、Fluxをまったく使用しない人に役立ちます。



1.サーバー応答の構造を繰り返すために状態は必要ありません



アプリケーションのローカル状態は、多くの場合、サーバーから受信したデータに基づいています。 アプリケーションを使用してサーバーデータを表示する場合、クライアントで同じ構造を使用する誘惑があります。



例として、オンラインストアを管理するアプリケーションを取り上げます。 マネージャーはそれを使用して商品を管理します。 製品のリストはサーバーから取得され、ビューでレンダリングするためにアプリケーションのローカル状態で保存する必要があります。 次のJSONがサーバーから送信されたとしましょう。



{ "total": 117, "offset": 0, "products": [ { "id": "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "title": "Blue Shirt", "price": 9.99 }, { "id": "aec17a8e-4793-4687-9be4-02a6cf305590", "title": "Red Hat", "price": 7.99 } ] }
      
      





商品のリストはオブジェクトの配列として送られてくるので、この状態でローカル状態で保存してみませんか?



この場合、商品のリストを転送するためのサーバーの配列の選択は、ページ分割、リストを分割ロード用のパーツに分割し、トラフィックを節約するためにデータを再送信しないようにすることに関連します。 これらはすべて正当な考慮事項ですが、一般に、アプリケーションのローカル状態をモデル化するときに従う考慮事項とは関係ありません。



2.オブジェクトは配列よりも望ましい



一般に、配列はアプリケーション状態で使用するのに最適なデータ構造ではありません。 リストから特定の製品を受信または更新する必要があると想像してください。 価格を変更する場合、またはサーバーから更新を適用する必要がある場合、特定の製品を検索するために配列を反復処理することは、製品オブジェクトのIDで受信するよりも便利ではありません。



この方法で、例のデータを更新できます。



 { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99 }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99 } } }
      
      





しかし、リスト内のアイテムの順序が重要な場合はどうでしょうか? たとえば、サーバー応答からの配列に示されている正確な順序で商品を表示する必要がある場合はどうなりますか? この場合、識別子の追加の配列を保存できます。



 { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99 }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99 } }, "productIds": [ "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "aec17a8e-4793-4687-9be4-02a6cf305590" ] }
      
      





注:この構造は、React Native ListViewコンポーネントに最適です。 cloneWithRowsの推奨バージョンは、この形式のみを想定しています。



3.州は、提出物がデータを受け入れる形式である必要はありません。



最終的に、表現は状態に基づいて描画されます。 状態データを表現に必要な形式で保存することにより、追加の変換を回避する機能は魅力的です。



オンラインストア管理システムを使用して例に戻りましょう。 各アイテムが在庫ありまたは在庫なしであると仮定します。 これを行うには、outOfStockフィールドを製品オブジェクトに追加します。



 { "id": "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "title": "Blue Shirt", "price": 9.99, "outOfStock": false }
      
      





アプリケーションは、不足しているすべての製品のリストを表示する必要があります。 覚えているように、React Native ListViewコンポーネントはcloneWithRowsメソッドに2つの引数を必要とします。文字列を持つオブジェクトと、これらの文字列のIDの配列です。 コンポーネントへの送信中にデータを変換しないように、このような構造を事前に準備し、アプリケーション状態に保存したいと思います。 状態構造は次のようになります。



 { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99, "outOfStock": false }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99, "outOfStock": true } }, "outOfStockProductIds": ["aec17a8e-4793-4687-9be4-02a6cf305590"] }
      
      





いいね? 実際、いいえ。



以前のように、理由は、アイデアが他の考慮事項によって導かれているためです。 その主なタスクは、ユーザーフォームにとって最も便利な形式でデータを表示することです。これは、必要な最小限のデータのみをアプリケーション状態に保存するという私たちの願望と矛盾するかもしれません。 さらに、異なるビューでは、1つのデータセットを異なる形式で表示できます。これにより、状態内のデータが重複する可能性があります。



これは次のポイントに私たちをもたらします。



4.状態のデータを複製しない



データの更新で整合性を維持するために2つの場所で同時に変更する必要がある場合、状態のデータは複製されます。 上記の例で、1つの製品が「在庫切れ」ステータスを受け取ったとします。 この更新に対応するには、製品オブジェクトのoutOfStockフィールドを変更し、そのIDをoutOfStockProductIds配列に追加する必要があります(2つの更新)。



問題は簡単に解決されます-余分なエンティティが削除されます。 データを1か所でのみ更新することにより、整合性を損なうリスクを排除します。



outOfStockProductIds配列を放棄した場合、ビューのデータを準備する方法を見つける必要があります。 Reduxの一般的なプラクティスは、 セレクターを使用することです:



 { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99, "outOfStock": false }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99, "outOfStock": true } } } // selector function outOfStockProductIds(state) { return _.keys(_.pickBy(state.productsById, (product) => product.outOfStock)); }
      
      





セレクターは、アプリケーションの状態を入力として受け取り、将来の使用のために変換された部分を返す純粋な関数です。 ダンアブラモフは、セレクターをレデューサーと一緒に配置することをお勧めします。これらは通常、密接に関連しているためです。 mapStateToPropsのビュー内でセレクターを使用します。



配列を削除するための実行可能な代替方法は、各アイテムのoutOfStockプロパティを削除することです。 この場合、商品の入手可能性に関する情報の単一ソースは、IDを持つ配列です。 ただし、パラグラフ2に従って、この配列をオブジェクトに変える方がよい場合があります。



 { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99 }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99 } }, "outOfStockProductMap": { "aec17a8e-4793-4687-9be4-02a6cf305590": true } } // selector function outOfStockProductIds(state) { return _.keys(state.outOfStockProductMap); }
      
      





5.セカンダリデータを状態に保持しない



データを更新するには整合性を維持するためにいくつかの場所で変更が必要なため、状態からの他のデータに基づく計算の結果として取得されたデータを保存すると、SSOT (単一の真実のソース)の原則に違反します。



アプリケーションに、製品の割引を指定する機能を追加します。 フィルターされた商品のリストをユーザーに表示する必要があります。このリストには、すべての商品が表示されるか、割引のある商品または割引のない商品が表示されます。



よくある間違いは、3つの配列の形式でデータを保存することです。各配列には、特定のフィルターのIDのセットが含まれます。 これらのセットは、製品のリストと現在のフィルターに関する情報に基づいて計算できるため、以前のようにセレクターを使用して生成する方がより正確です。



 { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99, "discount": 1.99 }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99, "discount": 0 } } } // selector function filteredProductIds(state, filter) { return _.keys(_.pickBy(state.productsById, (product) => { if (filter == "ALL_PRODUCTS") return true; if (filter == "NO_DISCOUNTS" && product.discount == 0) return true; if (filter == "ONLY_DISCOUNTS" && product.discount > 0) return true; return false; })); }
      
      





セレクターは、ビューが再描画される前にアプリケーションの状態が更新されるたびに実行されます。 複雑なセレクターがあり、パフォーマンスに注意する場合は、 メモ化を使用して計算結果をキャッシュします。 Reselectライブラリでは、これは既に実装されています。



6.関連オブジェクトを正規化する



私たちの仕事は、アプリケーションの開発とメンテナンスを快適にすることです。 同時に、彼の状態で作業することは便利なはずです。 状態の単純さは、データを持つオブジェクトが独立している場合に維持するのに便利ですが、オブジェクトが互いに接続されている場合はどうなりますか?



この例では、バイヤーが複数の製品を選択して注文できる注文管理システムを追加する必要があると想像してください。 このようなJSONをサーバーから注文のリストとともに取得するとします。



 { "total": 1, "offset": 0, "orders": [ { "id": "14e743f8-8fa5-4520-be62-4339551383b5", "customer": "John Smith", "products": [ { "id": "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "title": "Blue Shirt", "price": 9.99, "giftWrap": true, "notes": "It's a gift, please remove price tag" } ], "totalPrice": 9.99 } ] }
      
      





注文には複数の製品が含まれています。 互いにリンクする必要がある2つのエンティティがあることがわかります。 サーバーが提供する構造を使用してデータを1つずつ保存する必要はないことは既にわかっています。 この場合、それは商品のデータの重複につながります。



この場合の適切なアプローチは、データを正規化し、商品用と注文用の2つのオブジェクトを操作することです。 どちらのタイプのオブジェクトにもIDがあるため、このプロパティを使用してそれらを関連付けることができます。 したがって、アプリケーションの状態は次の形式を取ります。



 { "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99 }, "aec17a8e-4793-4687-9be4-02a6cf305590": { "title": "Red Hat", "price": 7.99 } }, "ordersById": { "14e743f8-8fa5-4520-be62-4339551383b5": { "customer": "John Smith", "products": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "giftWrap": true, "notes": "It's a gift, please remove price tag" } }, "totalPrice": 9.99 } } }
      
      





特定の注文のすべての商品を検索する場合、注文内の製品オブジェクトのキーを調べます。 各キーは、商品に関するより詳細な情報を取得するためにproductsByIdオブジェクトで参照できるIDです。 giftWrapなどの注文固有の製品情報は、製品注文オブジェクトにあります。



データを正規化するために、既製のライブラリーnormalizrがあります。



7.アプリケーションの状態は、メモリに保存されたデータベースとして扱うことができます



上記のヒントのほとんどはおそらくおなじみのものです。 従来のデータベースを設計するときに、同様の決定を行います



従来のデータベースの構造を設計するとき、セカンダリデータの重複と保存を避け、プライマリキー(ID)を使用してオブジェクトに似たテーブルのデータにインデックスを付け、複数のテーブル間の関係を正規化します。 上記で説明したすべてのこと。



メモリに格納されたデータベースとしてのアプリケーションの状態に対する態度は、データ構造に関するより多くの情報に基づいた決定を行うための正しいインストールを形成するのに役立ちます。



アプリケーションの状態を適切に扱う



記事の主なアイデアを強調すると、これがそれです。



命令型プログラミングでは、コードを最初に配置し、状態などの内部データ構造の「正しい」モデルの構築にあまり注意を払わない傾向があります。 多くの場合、アプリケーションの状態は、内部変数のセットとしてさまざまなマネージャー/コントローラーに散在しています。



宣言的なパラダイムでは、物事は異なります。 Reactおよび同様の環境では、システムが状態の変化に応答するため、アプリケーションの動作を制御するコードと同じくらい重要になります。 それがデータソースであるアクションと表現の両方がそれと連携します。 Reduxおよびその他のFlux実装は、この考えに基づいて構築され、アプリケーションをより予測可能にする新しい機能と制限を追加します。



アプリケーションの状態構造の設計には時間がかかります。 それがどれほど複雑になり、そのサポートと開発にどれだけの労力が費やされるかに注意しなければなりません。 そしてもちろん、コードの場合のように、タスクの効果的な対処を停止するアプリケーションの状態は、時々リファクタリングする必要があります。



翻訳者から:



Tal Kolによる投稿。 オリジナルはこちらから入手できます

コメントと翻訳のヒントを提供してくれたbniwredycに感謝します。




All Articles