web-dev-qa-db-ja.com

このタイプの検証を処理するために例外を使用するのはなぜ悪いのですか?それはコードをとてもきれいにするようです

.NET Core REST APIで作業しており、新しいユーザーアカウントを作成するためのサービスクラスを書いています。次のコードがあります。

_    public async Task<UserDto> RegisterNewUserAccount(CreateAccountDto userInfo)
    {
        EnsureUserDoesNotAlreadyExist(userInfo);
        EnsureRfidCardIsNotAlreadyClaimed(userInfo);

        var user = new UserDto()
        {
            Email = userInfo.EmailAddress.ToLower(),
            FirstAndLastName = userInfo.FirstAndLastName,
            Rank = 1200,
            JoinedTimestamp = DateTime.UtcNow.ToString()
        };

        await _usersRepository.CreateUser(user);

        var passwordHasher = new PasswordHasher<AccountCredentials>();
        var credentials = new AccountCredentials()
        {
            Email = userInfo.EmailAddress.ToLower(),
            HashedPassword = passwordHasher.HashPassword(null, userInfo.Password)
        };

        await _accountCredentialsRepository.InsertNewUserCredentials(credentials);
        await _emailSender.SendNewUserWelcomeEmail(userInfo);

        return await _usersRepository.GetUserWithEmail(user.Email);
    }

    private void EnsureUserDoesNotAlreadyExist(CreateAccountDto userInfo)
    {
        if (_usersRepository.GetUserWithEmail(userInfo.EmailAddress) != null)
        {
            throw new ResourceAlreadyExistsException("Email already in use.");
        }
    }

    private void EnsureRfidCardIsNotAlreadyClaimed(CreateAccountDto userInfo)
    {
        if (_usersRepository.GetUserWithRfid(userInfo.RfidNumber) != null)
        {
            throw new ResourceAlreadyExistsException("RFID card already in use.");
        }
    }
_

私にとって、このコードはクリーンで読みやすく、何をするのか明らかです。ただし、多くの人々はこのタイプのロジックに例外を使用することに厳しく反対しているようです。例外を通常の制御フローに使用するべきではないことは知っていますが、このような場合、それは自然なことのようです。変更を検討したことの1つは、2つのヘルパーメソッドの名前をThrowIfUserAlreadyExistsThrowIfRfidCardIsAlreadyClaimedに変更することです。これにより、これらのプライベートヘルパーメソッドは、条件が満たされています。

明らかに、パスワードが要件を満たしていることを確認するなど、一般的なユーザー入力の検証に例外を使用したくないのですが、このような場合、線がぼやけて見えます。

これが悪い習慣だと信じるなら、代わりにこのようなコードをどのように書くでしょうか?私のやり方でやるのはなぜ悪いのですか?

たとえば、DoesUserWithEmailAlreadyExist() check beforeを使用してそれを行おうとした場合、メソッドを呼び出し、そのユーザーがそのチェックと今の間に離れるとどうなりますか?さらに、巨大な混乱を招くことなく、代わりにここで何らかのタイプの戻りコードを返す方法を教えてください。もちろん、関数で使用できる戻り値の型は1つだけです。メソッドからのすべての応答をある種のCreateUserResultオブジェクトか何かにラップする必要があると私は真剣に言っているのですか?それは不愉快に思われ、コードがめちゃくちゃになるようです。

7
Bassinator

検証に例外を使用する場合の最初の問題は、通常、検証の結果として、同じデータで複数のエラーが発生する可能性があることを期待することです。

例外は、一部のコードが失敗し、結果としてスタックを巻き戻し、正常に処理して回復できるものまでエラーを渡すことが、 ハッピーパス 以外のシナリオに役立ちます。または、システムを良好な状態のままにします。

スタックの巻き戻しでは、例外が検証に適さなくなります。これは、データのエラーが検出された後にすべてのチェックが実行されるまで検証プロセス全体が続行されるため、ユーザーはデータの問題に関する完全なレポートを取得できるためです。単一のPOST/PUTリクエストから。

更新:コメントで指摘されているように、検証の問題をすべて1つの例外にまとめることで、例外を戻り値として扱うことも技術的に可能です。そして最後にそれを投げますが、私はこれも推奨も提唱もしません。

検証は、システムのユーザーが直面する機能的な機能であり、システムの動作のエラーではなく、データの問題を特定することを目的としています。検出された検証エラーは、システムが正しく動作していることを意味しますが、例外を使用して、システムまたはその依存関係の1つが正しく動作していないことを示す必要があります。


C#/ ASP.NET固有のもの:

ASP.NET Coreの特定のケースでは、可能な解決策は、ここで説明されているカスタムモデル検証を使用することです。 https://docs.Microsoft.com/en-us/aspnet/core/mvc/models/検証?view = aspnetcore-2.2

別の可能な代替策は、FluentValidationを使用することです: https://fluentvalidation.net/aspnet

9
Ben Cottrell

あなたの例では、2つの例外は例外的であるように思えます。すなわち。頻繁に投げられるとは思わないでしょう。

全体的なパターンの問題は通常あなたです[〜#〜] do [〜#〜]検証チェックが失敗することを期待します。

私はあなたが持っているのと同じ方法でこれをコード化します。しかし、私はさらに、同じ検証チェックbeforeを実行するコードを作成して、ユーザー作成リクエストを送信します。

これらのチェックは例外をスローせず、人間が読める検証エラーのリストを返します。問題のあるフィールドなどを強調表示するのに十分なメタデータが含まれています。

確かに、競合状態の場合は、実際のリクエストが届いたときに再確認する必要があります。しかし、それは今では例外的でありそうもない出来事です。ログに記録して、ユーザーに「エラー、再試行してください」と伝えます。

7
Ewan

発生する最大の実用的な問題は、パフォーマンスの違いです。戻り値のチェックと実行の停止、スタックの巻き戻し、その他の例外処理のコストは、およそ1桁です。

「それは大したことではありません」とあなたは言う、「DBへの呼び出しははるかに高価です!」

そしてそうです。問題は、あらゆる規模の場合、サーバーが通常のトラフィックからの通常の負荷を処理するように調整されることです。

ある嫌いな人があなたのサイトをリクエストで溢れさせることに決めたとき何が起こりますか-リクエストだけでなく、同じリクエストが何度も何度も既に存在するユーザーを作成するために?これで、通常よりも桁違いに多くのトラフィックが得られますandリクエストは、例外ルートを下っているため、Webサーバーからの桁違いに多くの負荷をかけます。

サーバーが溶けて、サイトがダウンし、悪循環が続いています。最新のクラウドシステムはほとんどのサービス拒否攻撃を処理できますが、一般的な例外ケースによるその指数関数的な効果は通常、それらを圧倒するのに十分です。 bestでは、応答時間を短縮し、それを処理するコストを増加させます。

例外はフロー制御に使用されません。

1
Telastyn

あなたが提供したコンテキストを持つあなたの特定の例では:
-コントローラーエンドポイントの使用方法の例なし

一部の "exceptional"検証が失敗したときに例外をスローしても問題ありません。例外ではない検証(一般的なユーザー入力)には例外を使用しないとおっしゃっていましたが
名前Ensure..は、例外をスローするメソッドには問題ないと思います。たとえば、HttpResponseMessage.EnsureSuccessStatusCodeは同じ命名方法を使用します。

コントローラーエンドポイントの使用方法の例なし

このAPIのクライアントは、可能性のある例外を処理する必要があり、それがすでに処理されていることを確認しますが、クライアントがサーバーエラー(500)と例外検証エラーを区別したい場合、現在の実装では500サーバーエラー応答はすべての場合に戻りました。
次のREST API HTTPステータスコード、ステータスコード422で応答を返すことは、クライアントがサーバーエラーで例外的な検証を行うのに役立つアプローチの1つであると考えられます。
つまり、コントローラーの内部ではtry .. catchを使用する必要があるということです。

Microsoftによるパフォーマンスの観点からの提案(カルトカーゴではない;)):
DA0007:制御フローに例外を使用しないでください

1
Fabio

この種類の検証での例外の使用は「悪い」ものです。なぜなら、与えられたコードでは、この種類の検証は実際には望まないからです。あなたが書く:

public async Task<UserDto> RegisterNewUserAccount(CreateAccountDto userInfo)
{
    EnsureUserDoesNotAlreadyExist(userInfo);
    ...
    await _usersRepository.CreateUser(user);

ここにあるのは競合状態です。

  • 最初に、新しいアイテムが一意性の要件を満たしているかどうかを確認します。
  • 次に、いくつかの(小さな)コードを実行してから、アイテムを挿入しようとします。
  • =>それまでに、一意性の要件は、いくつかの並列操作によってすでに違反されている可能性があります。

したがって、パフォーマンスの問題を無視して、 ベンが上に書いたもの に関連付けます:

検証に例外を使用する場合の最初の問題は、通常、検証の結果として、同じデータで複数のエラーが発生する可能性があることを期待することです。

あなたが持っている問題は次のとおりです:userに報告するために、すべての事前検証を収集し、この例外について確かに意味がありません。

RegisterNewUserAccount内の検証では、アトミックに検証する必要があります。つまり、CreateUserは成功して変更をコミットするか、失敗する必要があります。この例外はOKで、すべての入力を既に検証しているため、exceptionalです。これらはおそらく、バッキングストアからの一意の制約違反を報告するだけです。

1
Martin Ba

例外によるフローに対する主な議論は、コードの可読性の1つではなく、コードのパフォーマンスの1つです。

例外処理は高価です。そして、これは設計によるものであり、少なくともその設計の直接的な結果である詳細なデータロギングです。エラーが発生した場合、完全に詳細な情報(したがって、複雑なスタックトレース)の必要性が高く、高いパフォーマンスの必要性が少ないという考え方です。人々は通常、成功した操作からのみ良いパフォーマンスを期待します。
。Netが例外データの集計を行います。しかし、これは、例外をスローするたびに、.Netは常にそのデータを収集するための高価でスローモーションを通過することを意味し、それを回避することはできません。

誤解しないでください。2番目の読みやすさの議論もあります。かなり前に、アプリケーションのフローが読みやすく理解できないため、gotoロジックが必然的にスパゲッティコードにつながることに(ほぼ)普遍的に同意し始めました。 throwcatchは内部ではまだgotoステートメントにすぎません。

ただし、gotoは、エラーに直面したときに再び異なる優先順位に遭遇するため、許容できる例外処理アプローチです。たいていの場合、例外を高レベルにバブルアップさせ、多くの場合複数のレイヤーを横切るようにします。そして、gotoのような呼び出しを使用しない場合、すべてのレイヤー(つまり、渡すすべてのメソッド)がこれらのエラー戻り値を考慮する必要があることを意味します。すべての開発者がすべての可能なエラーを常に説明することを期待するのはほぼ不可能であり、時々いくつかの周辺のケースを処理することを必然的に忘れます。そして、彼らが常にそれを覚えていたとしても、これらのエラーケースの処理はよりかさばるコードにつながり、それを常に実装するためにかなりの追加時間がかかります。


要約すると、例外proがスローされる(予期しないエラーを処理する)場合とcon(意図されたビジネスロジックを処理する)場合があります。そして、エラーが本当にexceptionalである場合にのみ例外がスローされるようにすることは、開発者の責任です。つまり、ビジネスロジックで明示的に説明しなかったということです。エラーとすべてを明示的にカバーしようとするのは愚かです(極端な例として、HDDがまだアクセス可能であることを再確認するコードをどれくらいの頻度で作成しましたか?)

0
Flater