前回、入力値の必須検証から宣言に移行する方法の2つの例( 1、2 )を見ました。 2番目の例は、ストレージの側面について「あまりにも多くのことを知っています」と落とし穴( one 、 two )があります。 別の方法は、検証を3つの部分に分割することです。
- モデルバインディング:予期される
int
、string
が来ました-400を返しstring
- 値の検証:メールフィールドは
your@mail.com
の形式である必要があり、123Petya
が123Petya
を返します - ビジネスルールの検証:ユーザーのバスケットがアクティブであり、アーカイブにあることが予想されました。 422を返す
残念ながら、標準のASP.NET MVCバインディングメカニズムでは、型の不一致エラー(int
ではなく受信したstring
)と検証を区別しないため、400と422の応答コードを区別する場合は、自分で行う必要があります。 しかし、これはそれについてではありません。
ビジネスロジックレイヤーはどのようにしてエラーメッセージをコントローラーに返すことができますか?
Habr( one 、 two 、 three )による最も一般的な方法-例外をスローします。 したがって、「エラー」と「例外」の概念の間には、等号が配置されます。 さらに、「エラー」は広義の意味で解釈されます。これは、検証だけでなく、アクセス権とビジネスルールの検証でもあります。 そうですか? 間違いは「例外的な状況」ですか? 会計または税務会計に遭遇したことがあるなら、おそらく「調整」という特別な用語があることをご存じでしょう。 これは、前のレポート期間に誤った情報が送信されたため、修正する必要があることを意味しています。 つまり、会計の分野では、原則としてビジネスが存在することなく、エラーは第一級のオブジェクトです。 それらのために特別な用語が導入されています。 彼らは例外的な状況と呼ぶことができますか? いや これは正常な動作です。 人々は間違っています。 プログラマーは楽観的すぎます。 ピンクのメガネを脱ぐことはありません。
例外=エラー?
まあ、「例外」は単に悪い名前かもしれませんが、実際には「エラー」に対処するのに最適です。 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 Programming ( Habréでの翻訳 )で、エラーを処理するための代替アプローチを提案し、 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 Lippertの
SelectMany
の性質と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#バージョンでは、現在、「例外的でない」状況を処理するための組み込みメカニズムはありません。つまり、 発生する可能性があり、処理する必要があるエラー。 おそらく将来のバージョンでは、そのような機能を取得するでしょう。 特別な種類の例外を「例外的ではない状況」として予約するか、この記事のように他の特別な種類を導入します。