「基本的なレベルで関数型プログラミングを理解しているようで、簡単なプログラムを作成したように思えますが、実際のデータ、エラー処理などを備えた本格的なアプリケーションを作成するにはどうすればよいですか?」
これは非常によくある質問なので、このシリーズの記事では、設計、検証、エラー処理、永続性、依存関係管理、コード編成などをカバーする手順を説明することにしました。
まず、いくつかのコメントと注意事項:
- アプリケーション全体ではなく、1つのシナリオのみを説明します。 必要に応じてコードを拡張する方法が明らかになることを願っています。
- これは、データのストリーム処理に焦点を当てた、特別なトリックや高度な技術のない、意図的に非常に単純な命令です。 しかし、初心者の場合は、繰り返して期待どおりの結果を得ることができる一連の簡単な手順があると便利だと思います。 これが唯一の本当の方法だと言っているのではありません。 さまざまなシナリオでさまざまなアプローチが必要になります。もちろん、独自の専門知識の増加に伴い、この指示は単純すぎて制限されていることがわかります。
- オブジェクト指向設計からの移行を容易にするために、「テンプレート」、「サービス」、「依存性注入」などの使い慣れた概念を使用し、それらが機能的アプローチにどのように関連するかを説明します。
- また、命令はある程度命令的に意図的に行われます。 明示的な段階的なプロセスが使用されます。 このアプローチにより、OOPからFIへの移行が促進されることを願っています。
- 簡単にするため(およびF#スクリプトを使用する機能)、スタブをインフラストラクチャ全体にインストールし、UIとの直接の対話を避けます。
復習
この一連の記事で説明する予定の概要:
- スクリプトを関数に変換します。 最初の記事では、単純なシナリオを検討し、機能的なアプローチを使用してどのように実装できるかを見ていきます。
- 小さな機能を組み合わせます。 次の記事では、小さな関数を大きな関数に結合するための簡単なメタファーについて説明します。
- エラーの種類と種類を使用して設計します。 3番目の記事では、スクリプトに必要なタイプを作成し、エラー処理のための特別なタイプについて説明します。
- 依存関係を構成および管理します。 この記事では、すべての機能を接続する方法について説明します。
- 検証 この記事では、チェックを実装し、危険な外部の世界からタイプセーフティの暖かくふわふわした世界に変換するさまざまな方法について説明します。
- インフラ この記事では、ジャーナリング、外部コードの操作など、さまざまなインフラストラクチャコンポーネントについて説明します。
- 主題レベル。 この記事では、機能的な世界で主題指向設計がどのように機能するかについて説明します。
- プレゼンテーションレベル。 この記事では、UIで結果とエラーを表示する方法について説明します。
- 変化する要件に対応します。 この記事では、要件の変更に対処する方法と、それらがコードに与える影響について説明します。
さあ始めましょう
非常に単純な例を見てみましょう。つまり、Webサービスを通じて顧客情報を更新します。
そして、私たちの基本的な要件:
- ユーザーはいくつかのデータ(ユーザーID、メールボックスの名前とアドレス)を送信します。
- ボックスの名前と住所が正しいことを確認します。
- データベースでは、対応するユーザーレコードがメールボックスの名前とアドレスを更新します。
- メールボックスアドレスが変更された場合、このアドレスに確認書を送信します。
- 操作の結果をユーザーに表示します。
これは一般的なデータ処理シナリオです。 スクリプトを実行する特定のリクエストがあり、その後、リクエストからのデータがシステムを「流れ」、各ステップで処理されます。 このスクリプトは、エンタープライズソフトウェアで一般的であるため、例として使用します。
プロセスのコンポーネントの図は次のとおりです。
ただし、この説明はイベントの成功バージョンにすぎません。 現実は決して単純ではありません! ユーザーIDがデータベースに見つからない場合、郵送先住所が正しくない場合、またはデータベースにエラーがある場合はどうなりますか?
チャートを変更して、問題が発生する可能性のあるすべてのものに注意しましょう。
ご覧のとおり、さまざまな理由でスクリプトの各ステップでエラーが発生する場合があります。 この一連の記事の目標の1つは、エラーをエレガントに管理する方法を説明することです。
機能的思考
シナリオの手順を理解したので、機能的なアプローチを使用してそれを実装する方法は?
最初に、元のシナリオと機能的思考の違いを見てみましょう。
シナリオでは、通常、要求/応答モデルを意味します。 要求が送信され、応答が返されます。 何かがうまくいかなかった場合、アクションのフローは終了し、答えは「スケジュールより先」になります(翻訳者のメモ:これはプロセスに関するものであり、費やされた時間に関するものではありません)。
私が意味することは、スクリプトの単純化されたバージョンの図で見ることができます。
しかし、機能モデルでは、関数は次のように入力と出力のあるブラックボックスです。
このようなモデルにシナリオをどのように適合させることができますか?
一方向の流れ
まず、データの機能フローが前方にのみ広がっていることを認識する必要があります。 「スケジュールより先」を返すことはできません。
私たちの場合、これはすべてのエラーがスクリプトの終了前に代替パスに沿って送信されなければならないことを意味します。
これを行うとすぐに、ストリーム全体を単一の機能(ブラックボックス)に変えることができます。
もちろん、この大きな関数の内部を見ると、スクリプトの各ステージに1つ、互いに直列に接続された小さな関数(機能的方法論では「構成」)で構成されていることがわかります。
エラー管理
最後の図は、1つの正常終了と3つのエラー出力を示しています。 関数は4つではなく1つの出力しか持つことができないため、これは問題です。
それについて何ができますか?
答えは、各オプションが可能な出力の1つを表すUnionタイプを使用することです。 その場合、関数には実際には1つの出力しかありません。
結果を出力するための可能な型定義の例を次に示します。
type UseCaseResult = | Success | ValidationError | UpdateError | SmtpError
そして、次の4つの異なるオプションが含まれる単一の出力を示すやり直しチャートです。
エラー管理を簡素化
これで問題は解決しますが、各ステップのエラーの存在は脆弱であり、再利用設計にはあまり適していません。 もっと良くできますか?
はい! 本当に必要なのは2つの方法だけです。 1つは成功例であり、もう1つはすべてエラーの場合です。
type UseCaseResult = | Success | Failure
このタイプは非常に用途が広く、どのプロセスでも動作します! 実際、このタイプで作業するために、あらゆるシナリオに適した便利な関数の優れたライブラリを作成できることがすぐにわかります。
別のポイント-結果として、関数が返す結果、データはまったくなく、成功/失敗ステータスのみがあります。 関数の結果に実際の成功または失敗したオブジェクトが含まれるように、何かを修正する必要があります。 成功した型と失敗した型をユニバーサルとして宣言します(型パラメーターを使用)。
最後に、最終的なユニバーサルバージョン:
type Result<'TSuccess,'TFailure> = | Success of 'TSuccess | Failure of 'TFailure
実際、F#ライブラリにはすでに同様のタイプがあります。 Choiceと呼ばれます。 わかりやすくするために、この記事と以降の記事で以前に作成した結果タイプを引き続き使用します。 より深刻なタスクに近づくと、この問題に戻ります。
ここで、スクリプトを個々のステップでもう一度見てみると、各ステップのエラーを単一の「不良」パスに結合する必要があることがわかります。
これを行う方法は、次の記事のトピックです。
まとめとガイドライン
そのため、命令には次の規定があります。
ガイドライン
- 各シナリオは基本機能に相当します。
- スクリプト関数の戻り値の型は、成功と失敗の2つのオプションを持つ共用体です。
- スクリプト関数は、データストリームの個々のステップを表す一連の小さな関数から構築されます。
- すべてのステージのエラーは、単一のエラーパスに結合されます。