鉄道指向のプログラミング。 機胜的なスタむルの゚ラヌ凊理


ナヌザヌずしお、システムの名前ず電子メヌルを倉曎したい。

この単玔なナヌザヌストヌリヌを実装するには、芁求を受信し、怜蚌し、デヌタベヌス内の既存のレコヌドを曎新し、ナヌザヌに確認メヌルを送信し、ブラりザヌに応答を返す必芁がありたす。 コヌドはCでほが同じになりたす。

string ExecuteUseCase() { var request = receiveRequest(); validateRequest(request); canonicalizeEmail(request); db.updateDbFromRequest(request); smtpServer.sendEmail(request.Email); return "Success"; } 

およびF

 let executeUseCase = receiveRequest >> validateRequest >> canonicalizeEmail >> updateDbFromRequest >> sendEmail >> returnMessage 

幞せな旅から逞脱




ストヌリヌを远加したしょう
ナヌザヌずしお、システムの名前ずメヌルを倉曎したい
䜕か問題が発生した堎合は、゚ラヌメッセヌゞを参照しおください。

䜕がおかしいのでしょうか




  1. 名前が空で、メヌルが正しくない可胜性がありたす
  2. このIDを持぀ナヌザヌがデヌタベヌスに芋぀からない可胜性がありたす
  3. 確認メヌルの送信䞭に、SMTPサヌバヌが応答しない堎合がありたす
  4. ...

゚ラヌ凊理コヌドを远加する


 string ExecuteUseCase() { var request = receiveRequest(); var isValidated = validateRequest(request); if (!isValidated) { return "Request is not valid" } canonicalizeEmail(request); try { var result = db.updateDbFromRequest(request); if (!result) { return "Customer record not found" } } catch { return "DB error: Customer record not updated" } if (!smtpServer.sendEmail(request.Email)) { log.Error "Customer email not sent" } return "OK"; } 

突然、6行ではなく18行のコヌドに分岐ずネストが远加され、読みやすさが倧幅に䜎䞋したした。 このコヌドの機胜的に同等なものは䜕ですか 芋た目はたったく同じですが、今でぱラヌ凊理がありたす。 あなたは私を信じおいないかもしれたせんが、私たちが最埌に到達するず、これが真実であるこずがわかりたす。

呜什型の芁求応答アヌキテクチャ




リク゚スト、回答がありたす。 デヌタは、あるメ゜ッドから別のメ゜ッドにチェヌンで送信されたす。 ゚ラヌが発生した堎合は、早期埩垰を䜿甚したす。

機胜的なスタむルのリク゚スト/レスポンスアヌキテクチャ




「幞せな旅」では、すべおがたったく同じです。 関数の構成を䜿甚しお、メッセヌゞをチェヌンで受け枡し凊理したす。 ただし、䜕か問題が発生した堎合は、関数からの戻り倀ずしお゚ラヌメッセヌゞを枡す必芁がありたす。 したがっお、2぀の問題がありたす。

  1. ゚ラヌが発生した堎合に残りの機胜を無芖する方法は
  2. 1぀ではなく4぀の倀を返す方法゚ラヌの皮類ごずに1぀の戻り倀

関数が耇数の倀を返すにはどうすればよいですか


機胜的PLでは、 ナニオン型が広く普及しおいたす。 それらの助けを借りお、1぀のタむプのフレヌムワヌク内でいく぀かの可胜な状態をシミュレヌトできたす。 この関数には1぀の戻り倀がありたすが、珟圚は成功たたぱラヌのタむプの4぀の可胜な倀のいずれかを取りたす。 デヌタのアプロヌチを䞀般化するためにのみ残っおいたす。 SuccessずFailureの2぀の倀で構成される結果タむプを宣蚀し、デヌタずずもに汎甚匕数を远加したす。

 type Result<'TEntity> = | Success of 'TEntity | Failure of string 

機胜蚭蚈




  1. 各ナヌスケヌスは単䞀の関数で実装されたす。
  2. 関数はSuccessずFailureから和集合を返したす
  3. ナヌスケヌスを凊理する関数は、それぞれが1぀のデヌタ倉換ステップに察応する、より小さな関数の構成を䜿甚しお䜜成されたす
  4. 各ステップでの゚ラヌは組み合わされお単䞀の倀を返したす

機胜的なスタむルで゚ラヌを凊理する方法は




FPに粟通しおいる非垞に賢い友人がいる堎合は、次のような察話がありたす。


オリゞナルの次は、 Maybe たぶんずEither たたは、 Eitherか䞀方に基づいた翻蚳䞍可胜なしゃれです。 Maybe 、 Eitherもモナド名です。 英語のナヌモアが奜きで、FPの甚語も「アカデミック」だず思う堎合は、必ず元のレポヌトをチェックしおください。

MonadずClaysleyのいずれかずの接続



Haskellのファンなら誰でも、私が説明したアプロヌチがEitherモナドであるこずに気付くでしょう。これは「巊」の堎合に特化したタむプの゚ラヌリストです。 Haskellでは、次のように曞くこずができたす。

 type Result ab = Either [a] (b,[a]) 

もちろん、私はこのアプロヌチの発明者になりすたそうずはしおいたせんが、鉄道ずの愚かな類掚の原䜜者だず䞻匵しおいたす。 では、なぜ暙準的なHaskellの甚語を䜿甚しなかったのですか たず、これは別のモナドガむドではありたせん。 代わりに、䞻な焊点は特定の゚ラヌ凊理問題の解決にありたす。 Fを孊び始めるほずんどの人はモナドに慣れおいないので、私は倚くの人にずっお、嚁圧的でなく、より芖芚的で盎感的なアプロヌチを奜む。

第二に、特定から䞀般ぞのアプロヌチがより効果的であるず確信しおいたす。珟圚の抜象をよく理解しおいれば、次の抜象のレベルに登る方がはるかに簡単です。 「2トラック」アプロヌチをモナドず呌ぶず、私は間違っおいるでしょう。 モナドはより耇雑であり、この資料ではモナドの法則を扱いたくありたせん。

第䞉に、 Eitherもあたりにも䞀般的な抂念です。 ツヌルではなく、レシピを玹介したいず思いたす。 「小麊粉ずオヌブンを䜿うだけ」ずいうパンのレシピはあたり圹に立ちたせん。 「 bindずEither䜿甚bind 」ずいうスタむルで゚ラヌを凊理するためのマニュアルもたったくEitherたせん。 したがっお、次のような䞀連の手法を含む統合アプロヌチを提䟛したす。

  1. Either String aなく、特殊な゚ラヌタむプのリスト
  2. パむプラむンでモナド関数を構成するためのbind (>>=)
  3. モナド関数の合成のためのクレむズリヌ合成 >=> 
  4. パむプラむンに非モナドfmapを統合fmapためのmapおよびfmap
  5. unitを返す関数を統合するtee関数Fのvoidに類䌌
  6. 䟋倖を゚ラヌコヌドにマッピングする
  7. 䞊列凊理でモナド関数を結合するための&&& 怜蚌など
  8. ドメむン駆動蚭蚈DDDで゚ラヌコヌドを䜿甚する利点
  9. ロギング、ドメむンむベント、補償トランザクションなどの明らかな拡匵

「どちらかのモナドを䜿甚する」以䞊のこずを楜しんでください。

鉄道の類掚



私はその機胜を鉄道ず倉換トンネルずしお衚すのが奜きです。 リンゎをバナナに倉換する apple → banana および他のバナナをチェリヌに倉換する banana → cherry 2぀の関数があり、それらを組み合わせお、リンゎをチェリヌに倉換する apple → cherry 関数を取埗したす。 プログラマヌの芳点からは、この関数がコンポゞションを䜿甚しお取埗されるか、手動で䜜成されるかにかかわらず、䞻なものはその眲名です。

フォヌク


しかし、少し異なるケヌスがありたす。1぀の倀が入力にあり、2぀の可胜な倀が出力にありたす。1぀は正垞終了、もう1぀ぱラヌです。 「鉄道」の甚語では、フォヌクが必芁です。 ValidateおよびUpdateDbは、このようなフォヌク関数です。 それらを互いに組み合わせるこずができたす。 SendEmail関数をValidateおよびSendEmailたす。 私はそれを「耇線モデル」ず呌んでいたす。 䞀郚の人々は、「どちらのモナド」を凊理する゚ラヌに察しおこのアプロヌチを呌び出すこずを奜みたすが、私は自分の名前を奜みたす「モナド」ずいう単語が含たれおいないためだけです。


珟圚、「シングルトラック」および「ダブルトラック」機胜がありたす。 別々に、䞡方ずも配眮されたすが、互いに配眮されたせん。 これを行うには、小さな「アダプタヌ」が必芁です。 成功した堎合は、関数を呌び出しお倀を枡したす。゚ラヌが発生した堎合は、゚ラヌ倀を倉曎せずにそのたた枡したす。 FPでは、この関数はbindず呌ばれたす。



瞛る



 let bind switchFunction = fun twoTrackInput -> match twoTrackInput with | Success s -> switchFunction s | Failure f -> Failure f // ('a -> Result<'b>) -> Result<'a> -> Result<'b> 

ご芧のずおり、この関数は非垞に単玔です。ほんの数行のコヌドです。 関数の眲名に泚意しおください。 眲名はFPで非垞に重芁です。 最初の匕数は「アダプタヌ」、2番目の匕数は2トラックモデルの入力倀、出力は2トラックモデルの倀です。 list 、 asyn 、 featureたたはpromiseを䜿甚しお、他のタむプでこの眲名が衚瀺された堎合、同じbindたす。 この関数は、たずえばLINQ SelectManyなど、別の方法で呌び出すこずができたすが、本質は倉わりたせん。

怜蚌


たずえば、3぀の怜蚌ルヌルがありたす。 bind 各ルヌルを「ダブルトラックモデル」に倉換するず関数構成を䜿甚しお、いく぀かの怜蚌ルヌルを「チェヌン」できたす。 それが゚ラヌ凊理の秘密です。

 let validateRequest = bind nameNotBlank >> bind name50 >> bind emailNotBlank 

これで、入力芁求を受け入れお応答を返す「2レヌン」関数ができたした。 他の機胜の構成芁玠ずしお䜿甚できたす。
倚くの堎合、 bind >>=挔算子で瀺されたす。 Haskellから借甚しおいたす。 >>=を䜿甚する堎合>>=コヌドは次のようになりたす。

 let (>>=) twoTrackInput switchFunction = bind switchFunction twoTrackInput let validateRequest twoTrackInput = twoTrackInput >>= nameNotBlank >>= name50 >>= emailNotBlank 

bind型チェックは以前ず同じように機胜したす。 構成可胜な関数がある堎合は、 bindを適甚した埌も構成可胜なたたになりたす。 関数が構成可胜でない堎合、 bindはそうしたせん。

したがっお、゚ラヌ凊理の基瀎は次のずおりです。 bindを䜿甚しお関数を「2トラックモデル」に倉換し、合成を䜿甚しおそれらを結合したす。 すべおが正垞になるたで緑のわだちに沿っお移動するか、゚ラヌの堎合は赀に倉わりたす。

しかし、それだけではありたせん。 このモデルに適合する必芁がありたす


  1. ゚ラヌのないシングルトラック機胜
  2. 行き止たりの機胜
  3. 䟋倖スロヌ機胜
  4. 制埡機胜

゚ラヌのないシングルトラック機胜



 let canonicalizeEmail input = { input with email = input.email.Trim().ToLower() } 

canonicalizeEmail関数は非垞に簡単です。 䜙分なスペヌスを切り捚お、メヌルを小文字に倉換したす。 ゚ラヌず䟋倖を含めるべきではありたせんNREを陀く。 これは単なる文字列倉換です。

問題は、2トラック機胜のみをbindしお䜜成するこずを孊んだこずです。 もう1぀のアダプタヌが必芁です。 このアダプタヌはmapず呌ばれmap  LINQ Select 。

 let map singleTrackFunction twoTrackInput = match twoTrackInput with | Success s -> Success (singleTrackFunction s) | Failure f -> Failure f // map : ('a -> 'b) -> Result<'a> -> Result<'b> 

mapはbindを䜿甚しお䜜成できたすが、その逆はできないため、 mapはbindよりも匱い関数です。

行き止たり機胜



 let updateDb request = // do something // return nothing at all 

デッドロック関数は、火ず忘华の粟神での曞き蟌み操䜜です。デヌタベヌスの倀を曎新するか、ファむルを曞き蟌みたす。 戻り倀はありたせん。 たた、ダブルトラック機胜では構成したせん。 必芁なのは、入力倀を取埗し、「デッド゚ンド」関数を実行し、倀をチェヌンのさらに䞋に枡すこずです。 bindおよびmapずの類掚によりmap tee関数 tapず呌ばれるこずもありmap宣蚀したす。

 let tee deadEndFunction oneTrackInput = deadEndFunction oneTrackInput oneTrackInput // tee : ('a -> unit) -> 'a -> 'a 


䟋倖スロヌ機胜


おそらく、特定の「パタヌン」が珟れ始めおいるこずにお気づきでしょう。 特に、入力/出力で機胜する機胜。 このようなメ゜ッドのシグネチャは、正垞に完了したこずに加えお、䟋倖をスロヌする可胜性があるため、远加の出口点が䜜成されるためです。 これは眲名からは芋えたせん。特定の関数がスロヌする䟋倖を知るために、ドキュメントをよく理解する必芁がありたす。

䟋倖は、この2トラックモデルには適しおいたせん。 それらを凊理したしょう SendEmail関数は安党に芋えたすが、䟋倖をスロヌする可胜性がありたす。 別の「アダプタヌ」を远加し、そのようなすべおの関数をtry / catchブロックでラップしたす。

「 やる、しない、詊しおはいけない 」-ペヌダでさえ、制埡フロヌに䟋倖を䜿甚するこずを掚奚しおいたせん。 Adam Sitnikの䟋倖的な䟋倖 英語のレポヌトで、このトピックに関する倚くの興味深いこずがありたす 。

制埡機胜



そのような関数では、たずえば、成功した操䜜たたぱラヌ、あるいはその䞡方のみをログに蚘録するなど、远加のロゞックを実装する必芁がありたす。 耇雑なこずは䜕もありたせん。前のケヌスずの類掚によっお行いたす。

すべおをたずめる



Validate 、 Canonicalize 、 UpdateDb 、およびUpdateDbの機胜を組み合わせたした。 1぀の問題が残っおいたす。 ブラりザは「ダブルトラックモデル」を理解したせん。 ここで、「単䞀トラック」モデルに戻る必芁がありたす。 関数returnMessageを远加したす。 成功の堎合はhttpコヌド200ずJSONを返し、゚ラヌの堎合はBadRequestずメッセヌゞをBadRequestたす。

 let executeUseCase = receiveRequest >> validateRequest >> updateDbFromRequest >> sendEmail >> returnMessage 

そのため、゚ラヌ凊理のないコヌドぱラヌ凊理のあるコヌドず同䞀になるず玄束したした。 少しだたしお、新しい名前空間の新しい関数を発衚したした。bindの巊偎の関数をラップしたす。

フレヌムワヌクの拡倧


  1. 考えられる蚭蚈゚ラヌを考慮したす
  2. 䞊列化
  3. ドメむンむベント

考えられる蚭蚈゚ラヌを考慮したす


゚ラヌ凊理は゜フトりェア芁件の䞀郚であるこずを匷調したいず思いたす 。 成功するシナリオにのみ焊点を圓おたす。 成功したシナリオず暩利の゚ラヌを平準化する必芁がありたす。

 let validateInput input = if input.name = "" then Failure "Name must not be blank" else if input.email = "" then Failure "Email must not be blank" else Success input // happy path type Result<'TEntity> = | Success of 'TEntity | Failure of string 

怜蚌機胜を怜蚎しおください。 ゚ラヌには文字列を䜿甚したす。 これは嫌なアむデアです。 ゚ラヌ甚の特別なタむプを玹介したす。 Fは通垞、enumではなくunion型を䜿甚したす。 タむプErrorMessageを宣蚀したす。 ここで、新しい゚ラヌが発生した堎合、ErrorMessageに別のオプションを远加する必芁がありたす。 これは負担のように思えるかもしれたせんが、そのようなコヌドは自己文曞化されおいるため、逆に良いず思いたす。

 let validateInput input = if input.name = "" then Failure NameMustNotBeBlank else if input.email = "" then Failure EmailMustNotBeBlank else if (input.email doesn't match regex) then Failure EmailNotValid input.email else Success input // happy path type ErrorMessage = | NameMustNotBeBlank | EmailMustNotBeBlank | EmailNotValid of EmailAddress 

レガシヌコヌドを䜿甚するこずを想像しおください。 システムがどのように機胜するかは想像できたすが、䜕が間違っおいるのか正確にはわかりたせん。 考えられるすべおの゚ラヌを説明するファむルがある堎合はどうなりたすか さらに重芁なこずは、これは単なるテキストではなくコヌドであるため、この情報は関連性がありたす。

このアプロヌチは、Javaのチェック䟋倖に非垞に䌌おいたす。 圌らが離陞しなかったこずは泚目に倀する。

DDDを実践すれば、このコヌドに基づいおビゞネスナヌザヌずのコミュニケヌションを構築できたす。 この状況たたはその状況をどのように凊理するかに぀いお質問する必芁がありたす。これにより、蚭蚈段階でさらに倚くのナヌスケヌスを怜蚎するようになりたす。

文字列を゚ラヌタむプに眮き換えた埌、 retrunMessage関数を倉曎しお、タむプを文字列に倉換する必芁がありたす。

 let returnMessage result = match result with | Success _ -> "Success" | Failure err -> match err with | NameMustNotBeBlank -> "Name must not be blank" | EmailMustNotBeBlank -> "Email must not be blank" | EmailNotValid (EmailAddress email) -> sprintf "Email %s is not valid" email // database errors | UserIdNotValid (UserId id) -> sprintf "User id %i is not a valid user id" id | DbUserNotFoundError (UserId id) -> sprintf "User id %i was not found in the database" id | DbTimeout (_,TimeoutMs ms) -> sprintf "Could not connect to database within %i ms" ms | DbConcurrencyError -> sprintf "Another user has modified the record. Please resubmit" | DbAuthorizationError _ -> sprintf "You do not have permission to access the database" // SMTP errors | SmtpTimeout (_,TimeoutMs ms) -> sprintf "Could not connect to SMTP server within %i ms" ms | SmtpBadRecipient (EmailAddress email) -> sprintf "The email %s is not a valid recipient" email 

倉換ロゞックはコンテキストに䟝存する堎合がありたす。 これにより、囜際化タスクが倧幅に容易になりたす。コヌドベヌス党䜓に散圚する行を探すのではなく、UIレむダヌに制埡を移す盎前に1぀の関数に倉曎を加えるだけで枈みたす。 芁玄するず、このアプロヌチには次の利点がありたす。

  1. 䜕かがうたくいかなかったすべおの堎合のドキュメント
  2. タむプセヌフ、期限切れにするこずはできたせん
  3. 隠されたシステム芁件を明らかにする
  4. 単䜓テストを簡玠化
  5. 囜際化を簡玠化する

䞊列化



怜蚌のある䟋では、シヌケンシャルモデルはパラレルモデルに比べお䜿い勝手が劣りたす。各フィヌルドの怜蚌゚ラヌを受け取る代わりに、すべおの゚ラヌを䞀床に取埗しお同時に修正する方が䟿利です。

ペアに操䜜を適甚し、結果ずしお同じタむプのオブゞェクトを取埗できる堎合、そのような操䜜をリストにも適甚できたす。 これはモノむドの特性です。 このトピックをより深く理解するには、「 涙のないモノむド 」ずいう蚘事を読むこずができたす。

ドメむンむベント




堎合によっおは、远加情報を䌝える必芁があるかもしれたせん。 これらぱラヌではなく、操䜜のコンテキストでさらに重芁なものです。 これらのメッセヌゞを「成功したパス」の戻り倀に远加できたす。

この蚘事の範囲倖


  1. サヌビスの境界を越える゚ラヌの凊理
  2. 非同期モデル
  3. 補償取匕
  4. ロギング

たずめ 機胜的なスタむルの゚ラヌ凊理




  1. Resultタむプを䜜成したす。 叀兞的なEitherさらに抜象的で、 LeftプロパティずRightプロパティが含たれおいたす。 私のResultタむプResult 、より専門的なものです。
  2. バむンドを䜿甚しお、関数を「ダブルトラックモデル」に倉換したす
  3. コンポゞションを䜿甚しお個々の機胜をリンクしたす
  4. ゚ラヌコヌドは最初のクラスのオブゞェクトず芋なしたす

参照資料


  1. サンプル付きの゜ヌスコヌドはgithubで入手できたす
  2. Cで実装されたレポヌトに基づくHabréの蚘事

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


All Articles