エラーと例外について





前回、入力値の必須検証から宣言に移行する方法の2つの例( 1、2 )を見ました。 2番目の例は、ストレージの側面について「あまりにも多くのことを知っています」と落とし穴( onetwo )があります。 別の方法は、検証を3つの部分に分割することです。



  1. モデルバインディング:予期されるint



    string



    が来ました-400を返しstring



  2. 値の検証:メールフィールドはyour@mail.com



    の形式である必要があり、 123Petya



    123Petya



    を返します
  3. ビジネスルールの検証:ユーザーのバスケットがアクティブであり、アーカイブにあることが予想されました。 422を返す


残念ながら、標準のASP.NET MVCバインディングメカニズムでは、型の不一致エラー( int



ではなく受信したstring



)と検証を区別しないため、400と422の応答コードを区別する場合は、自分で行う必要があります。 しかし、これはそれについてではありません。


ビジネスロジックレイヤーはどのようにしてエラーメッセージをコントローラーに返すことができますか?



Habr( onetwothree )による最も一般的な方法-例外をスローします。 したがって、「エラー」と「例外」の概念の間には、等号が配置されます。 さらに、「エラー」は広義の意味で解釈されます。これは、検証だけでなく、アクセス権とビジネスルールの検証でもあります。 そうですか? 間違いは「例外的な状況」ですか? 会計または税務会計に遭遇したことがあるなら、おそらく「調整」という特別な用語があることをご存じでしょう。 これは、前のレポート期間に誤った情報が送信されたため、修正する必要があることを意味しています。 つまり、会計の分野では、原則としてビジネスが存在することなく、エラーは第一級のオブジェクトです。 それらのために特別な用語が導入されています。 彼らは例外的な状況と呼ぶことができますか? いや これは正常な動作です。 人々は間違っています。 プログラマーは楽観的すぎます。 ピンクのメガネを脱ぐことはありません。



例外=エラー?



まあ、「例外」は単に悪い名前かもしれませんが、実際には「エラー」に対処するのに最適です。 MSDNでさえ、「例外」を「ランタイムエラー」と定義しています。 見てみましょう。 未処理の例外が発生した場合、プログラムはどうなりますか? 異常終了。 実際、すべての未処理の例外がグローバルに処理されるため、Webアプリケーションは終了しません。 すべてのサーバープラットフォームは、「未処理」エラーをサブスクライブする機能を提供します。 ビジネスロジックでエラーが発生した場合、プログラムを終了する必要がありますか? 場合によっては、たとえば、高頻度取引用のソフトウェアを開発していて、取引アルゴリズムに問題が発生した場合です。 間違っている場合、どれだけ速く意思決定をしてもかまいません。 そして、ユーザー入力にエラーがある場合は? いいえ、意味のあるメッセージをユーザーに表示する必要があります。 したがって、エラーは致命的または「それほどではありません」。 1つのタイプを使用して両方を指定するのは困難です。



2つのプロジェクトがサポートされていると想像してください。 両方とも、データベース内の未処理の例外をすべて記録します。 最初の例外は非常にまれです。1か月に1〜2回、2番目に1日あたり数百回です。 最初のケースでは、ログを非常に慎重に調査します。 ログに何かが表示される場合、根本的な問題があり、場合によってはシステムが未定義の状態になる可能性があります。 2番目では、信号対雑音比は「壊れています」。 ログに常にエラーがいっぱいになっている場合、システムが正常に動作しているか、乱流ゾーンに入ったかどうかを調べる方法は?



BusinessLogicException



タイプを作成し、個別にログに記録することができます(またはログに記録しません)。 次に、 HttpException



DbValidationException



などに対して同様のフェイントをDbValidationException



します。 うーん、どの例外をキャッチし、どの例外をキャッチしないかを覚えておく必要があります。 まさに、Javaには例外がチェックされています。.NETにインポートしましょう。 すべての例外をキャッチして処理できるわけではなく、TPL例外 を処理する機能を忘れないわけではないことを考慮する必要があるだけです。 そして、彼のように、まあ、このパフォーマンス



例外= goto?



例外の広範な使用に対するもう1つの議論は、 gotoとの類似性です。 メソッドシグネチャはどの例外を内部にスローできるかを明らかにしないため、コールチェーンのどこでキャッチされるかを見つける方法はありません。 さらに、例外のある言語のメソッドシグネチャはネゴシエートさませんRequestDto -> IActionResult



ではなく、 RequestDto -> IActionResult | Exception



RequestDto -> IActionResult



を記述する方が適切RequestDto -> IActionResult | Exception



RequestDto -> IActionResult | Exception



:メソッドが成功するか、何かがうまくいかない可能性があります。



3層アーキテクチャでの例外処理



エラーが発生した場所が正確にわからないため、エラーを適切に処理したり、ユーザーに意味のあるメッセージを作成したり、補正アクションを適用したりすることはできません。



したがって、すべてのタイプの「エラー」のビジネスロジックレイヤーで例外が使用される場合、各コントローラーメソッドをtry / catch



ブロックでラップするか、アプリケーションレベルで処理メソッドを再定義する必要があります。 最初のオプションは悪いです。どこでもtry / catch



を複製し、 try / catch



エラーのタイプを監視する必要があるからです。 2つ目は、実行コンテキストが失われていることです。



Scott Vlashinは、彼のレポートRailway Oriented ProgrammingHabréでの翻訳 )で、エラーを処理するための代替アプローチを提案し、 vkhorikov をC#に適合させ ました 。 私はこのオプションをわずかに改良する自由を取りました。



結果の変更



 public class Result { public bool Success { get; private set; } public string Error { get; private set; } public bool Failure { get { return !Success; } } protected Result(bool success, string error) { Contracts.Require(success || !string.IsNullOrEmpty(error)); Contracts.Require(!success || string.IsNullOrEmpty(error)); Success = success; Error = error; } //... }
      
      





string



型は、エラーの処理にはあまり便利ではありません。 文字列をタイプFailure



置き換えます。 Scottのバリアントとは異なり、 Failure



はユニオン型ではなく、通常のクラスになります。 エラー処理のパターンマッチングはポリモーフィズムに置き換えられます。 エラーに関する追加情報を保存するために、 Data



プロパティを使用します。 多くの場合、このデータはシリアル化する必要があるだけなので、特定のタイプはそれほど重要ではありません。



 public class Failure { public Failure(params Failure[] failures) { if (!failures.Any()) { throw new ArgumentException(nameof(failures)); } Message = failures.Select(x => x.Message).Join(Environment.NewLine); var dict = new Dictionary<string, object>(); for(var i = 0; i < failures.Length; i++) { dict[(i + 1).ToString()] = failures[i]; } Data = new ReadOnlyDictionary<string, object>(dict); } public Failure(string message) { Message = message; } public Failure(string message, IDictionary<string, object> data) { Message = message; Data = new ReadOnlyDictionary<string, object>(data); } public string Message { get; } public ReadOnlyDictionary<string, object> Data { get; protected set; } }
      
      





検証エラーとアクセス権のための特別な継承クラスを宣言します。



 public class ValidationFailure: Failure { public ValidationResult[] ValidationResults { get; } public ValidationFailure(IEnumerable<ValidationResult> validationResults) : base(ValidationResultsToStrings(validationResults)) { ValidationResults = validationResults?.ToArray(); if (ValidationResults == null || !ValidationResults.Any()) { throw new ArgumentException(nameof(validationResults)); } Data = new ReadOnlyDictionary<string, object>( ValidationResults.ToDictionary( x => x.MemberNames.Join(","), x => (object)x.ErrorMessage)); } private static string ValidationResultsToStrings( IEnumerable<ValidationResult> validationResults) => validationResults .Select(x => x.ErrorMessage) .Join(Environment.NewLine); }
      
      





演算子をオーバーロードし、 Value



を非表示にします



&, | true false



オーバーロードを追加します &&



||



ように&, | true false



。 値を閉じて、代わりにReturn



関数を提供します。 現在、ミスをしてIsFaulted



プロパティをチェックしないことは不可能です。このメソッドは、 T



Failure



両方をTDestination



型にTDestination



することをコミットします。 これにより、チェックを忘れる可能性のある戻りコードに関する問題が解決されます。 エラーバリアントを処理しないと、結果を取得できません。



 public class Result { public static implicit operator Result (Failure failure) => new Result(failure); // https://stackoverflow.com/questions/5203093/how-does-operator-overloading-of-true-and-false-work public static bool operator false(Result result) => result.IsFaulted; public static bool operator true(Result result) => !result.IsFaulted; public static Result operator &(Result result1, Result result2) => Result.Combine(result1, result2); public static Result operator |(Result result1, Result result2) => result1.IsFaulted ? result2 : result1; public Failure Failure { get; private set; } public bool IsFaulted => Failure != null; }
      
      





Web操作のコンテキストでは、変換メソッドの実装は次のようになります。



 result.Return<IActionResult>(Ok, x => BadRequest(x.Message));
      
      





または、 Scottの例の場合 :リクエストを受信し、検証を実行し、データベース内の情報を更新し、成功した場合、次のような確認メールを送信します。



 public IActionResult Post(ChangeUserNameCommand command) { var res = command.Validate(); if (res.IsFaulted) return res; return ChangeUserName(command) .OnSuccess(SendEmail) .Return<IActionResult>(Ok, x => BadRequest(x.Message)); }
      
      





LINQ構文のサポート



さらにステップがある場合、行if(res.IsFaulted) return res;



各ステップの後に繰り返す必要があります。 これを避けたいです。 これは、 Eric LippertSelectMany



の性質とMの文字に関する一連の記事にとって絶好の瞬間です。一般に、LINQ構文はIEnumerable



だけでなく、他の型もサポートします。 主なSelectMany



別名Bind



SelectMany



実装するSelectMany



。 恐ろしいテンプレートコードを追加します。 ここでは、 bind



仕組みについて詳しく説明しません。
興味がある場合は、LippertまたはVlashinをお読みください



 public static class ResultExtensions { public static Result<TDestination> Select<TSource, TDestination>( this Result<TSource> source, Func<TSource, TDestination> selector) => source.IsFaulted ? new Result<TDestination>(source.Failure) : selector(source.Value); public static Result<TDestination> SelectMany<TSource, TDestination>( this Result<TSource> source, Func<TSource, Result<TDestination>> selector) => source.IsFaulted ? new Result<TDestination>(source.Failure) : selector(source.Value); public static Result<TDestination> SelectMany<TSource, TIntermediate, TDestination>( this Result<TSource> result, Func<TSource, Result<TIntermediate>> inermidiateSelector, Func<TSource, TIntermediate, TDestination> resultSelector) => result.SelectMany<TSource, TDestination>(s => inermidiateSelector(s) .SelectMany<TIntermediate, TDestination>(m => resultSelector(s, m))); }
      
      





少し変わっているように見えますが、コールチェーンを構築し、それらを1つのパイプに結合することができます。 さらに、すべてのif(result.IsFaulted)



は、LINQ構文を使用して「 if(result.IsFaulted)



」で実行されます。



 public Result<UserNameChangedEvent> Declarative(ChangeUserNameCommand command) => from validatedCommand in command.Validate() from domainEvent in ChangeUserName(validatedCommand).OnSuccess(SendEmail) select domainEvent;
      
      





おわりに



例外を拒否することを強くお勧めしません。 これは、驚くべき「例外的な状況」-私たちがまったく予期していなかった間違いのための非常に良いツールです。 システムが未定義の状態に移行するのを防ぐのに役立ち、アプリケーションの通常/緊急操作の優れた指標として機能します。 ただし、どこでも例外を使用すると、このツールを使用できなくなります。 定義により、通常の状況は例外とは見なされません。



C#バージョンでは、現在、「例外的でない」状況を処理するための組み込みメカニズムはありません。つまり、 発生する可能性があり、処理する必要があるエラー。 おそらく将来のバージョンでは、そのような機能を取得するでしょう。 特別な種類の例外を「例外的ではない状況」として予約するか、この記事のように他の特別な種類を導入します。



All Articles