有益な例外とクリーンなコードのバランスをとる良い方法は何ですか?


11

公開SDKでは、例外が発生する理由について非常に有益なメッセージを提供する傾向があります。例えば:

if (interfaceInstance == null)
{
     string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    throw new InvalidOperationException(errMsg);
}

ただし、コードが何をしているのかではなく、エラーメッセージに重点を置く傾向があるため、コードの流れが煩雑になる傾向があります。

同僚は、次のようなものにスローする例外の一部をリファクタリングし始めました:

if (interfaceInstance == null)
    throw EmptyConstructor();

...

private Exception EmptyConstructor()
{
    string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    return new InvalidOperationException(errMsg);
}

これにより、コードロジックが理解しやすくなりますが、エラー処理を行うための多くのメソッドが追加されます。

「長い例外メッセージの混乱ロジック」問題を回避する他の方法は何ですか?私は主に慣用的なC#/。NETについて質問していますが、他の言語がそれをどのように管理するかについても役立ちます。

[編集]

それぞれのアプローチの長所と短所もあるといいですね。


4
私の同僚のソリューションは非常に良いものであり、そのような追加のメソッドが本当にたくさんある場合、少なくともそれらのいくつかを再利用できると思います。プログラムのわかりやすいビルディングブロックを作成する限り、メソッドに適切な名前が付けられていれば、小さなメソッドがたくさんあるのは問題ありません。これが当てはまるようです。
Doc Brown、

@DocBrown-ええ、私はアイデアが好きです(長所/短所とともに以下の回答として追加されます)が、インテリセンスとメソッドの潜在的な数の両方にとって、それも同様に乱雑に見え始めます。
FriendlyGuy

1
思想:Doがメッセージにしないすべての例外の詳細のキャリア。プロパティの使用、「ピッキーな」例外のキャッチ、呼び出しコードのキャッチと独自のコンテキストの追加、およびキャプチャされたコールスタックの組み合わせはすべて、詳細なメッセージをはるかに少なくできる情報を提供します。最後に、「例外構築」メソッドに渡す詳細を提供することで有望に見えます。Exception.DataSystem.Reflection.MethodBase
radarbob

@radarbobは、おそらく例外が冗長すぎることを示唆していますか?おそらく、それをロギングのようにしすぎているのかもしれません。
FriendlyGuy

1
@MackleChan、私は、ここでのパラダイムは「起こったことを正確に伝え、必然的に文法的に正しく、AIのふりをすることを試みるメッセージに情報を入れることです: "空のコンストラクターを介して動作しましたが、.."本当に?私の内部の法医学コーダーは、これをスタックトレースを失う再スローの一般的なエラーとの認識がないことの進化的な結果と見なしていますException.Data。重点はテレメトリをキャプチャすることです。ここでのリファクタリングは問題ありませんが、問題を見逃しています。
レーダーボブ2013

回答:


10

なぜ特別な例外クラスがないのですか?

if (interfaceInstance == null)
{
    throw new ThisParticularEmptyConstructorException(<maybe a couple parameters>);
}

これにより、フォーマットと詳細が例外自体にプッシュされ、メインクラスが整理された状態になります。


1
長所:非常にクリーンで整理され、各例外にわかりやすい名前を付けます。短所:潜在的に多くの追加の例外クラス。
FriendlyGuy

重要な場合は、限られた数の例外クラスを作成し、例外が発生した場所のクラスをパラメーターとして渡し、より一般的な例外クラス内に巨大なスイッチを配置することができます。しかし、それはかすかなにおいがします-私がそこに行くかどうかはわかりません。
ptyx 2013

それは本当ににおいがします(クラスとの密結合の例外)。カスタマイズ可能な例外メッセージと例外の数のバランスを取るのは難しいと思います。
FriendlyGuy

7

Microsoftは(.NETソースを参照して)リソース/環境文字列を使用する場合があります。たとえば、ParseDecimal次のとおりです。

throw new OverflowException(Environment.GetResourceString("Overflow_Decimal"));

長所:

  • 例外メッセージを一元化し、再利用を可能にする
  • 例外メッセージ(おそらくコードには関係ありません)をメソッドのロジックから遠ざける
  • スローされる例外のタイプは明らかです
  • メッセージをローカライズできます

短所:

  • 1つの例外メッセージが変更されると、すべてが変更されます
  • 例外メッセージは、例外をスローするコードでは簡単に利用できません。
  • メッセージは静的であり、どの値が間違っているかについての情報は含まれていません。それをフォーマットしたい場合は、コードがより乱雑になります。

2
例外テキストのローカライズ:あなたは大きなメリット取り残さ
26の17の

@ 17of26-良い点、それを追加しました。
FriendlyGuy

私はあなたの答えを賛成しましたが、この方法で作成されたエラーメッセージは「静的」です。OPが彼のコードで行っているように、それらに修飾子を追加することはできません。したがって、あなたは彼の機能の一部を効果的に省略しました。
Robert Harvey

@RobertHarvey-私はそれをコンとして追加しました。これは、組み込みの例外がローカル情報を提供しない理由を説明しています。また、私はOPです(私はこの解決策を知っていましたが、他の人がより良い解決策を持っているかどうか知りたいと思っていました)。
FriendlyGuy

6
@ 17of26は開発者として、ローカライズされた例外を情熱をもって憎んでいます。たとえば、できるようになる前に毎回それらのローカライズを解除する必要があります。ソリューションのグーグル。
Konrad Morawski、

2

パブリックSDKシナリオの場合、有益なエラー、静的チェックを提供し、XMLドキュメントに追加するドキュメントを生成したり、Sandcastleが生成したヘルプファイルを追加したりできるため、Microsoftコードコントラクトの使用を強く検討します。Visual Studioのすべての有償バージョンでサポートされています。

追加の利点は、顧客がC#を使用している場合、コードコントラクト参照アセンブリを利用して、コードを実行する前でも潜在的な問題を検出できることです。

コードコントラクトの完全なドキュメントはこちらです。


2

私が使用するテクニックは、検証組み合わせてアウトソーシングし、ユーティリティ関数にまとめてスローすることです。

最も重要な利点の1つは、ビジネスロジックで1つのライナーに削減されることです。

さらに削減できない限り、ビジネスロジックからすべての引数の検証とオブジェクト状態のガードを排除し、運用上の例外条件のみを維持することで、より良い結果が得られると思います。

もちろん、それを行う方法があります-強く型付けされた言語、「いつでも無効なオブジェクトを許可しない」設計、契約による設計など。

例:

internal static class ValidationUtil
{
    internal static void ThrowIfRectNullOrInvalid(int imageWidth, int imageHeight, Rect rect)
    {
        if (rect == null)
        {
            throw new ArgumentNullException("rect");
        }
        if (rect.Right > imageWidth || rect.Bottom > imageHeight || MoonPhase.Now == MoonPhase.Invisible)
        {
            throw new ArgumentException(
                message: "This is uselessly informative",
                paramName: "rect");
        }
    }
}

public class Thing
{
    public void DoSomething(Rect rect)
    {
        ValidationUtil.ThrowIfRectNullOrInvalid(_imageWidth, _imageHeight, rect);
        // rest of your code
    }
}

1

[注]コメントがある場合は、質問から回答にコピーしました。

各例外をクラスのメソッドに移動し、書式設定が必要な引数を受け取ります。

private Exception EmptyConstructor()
{
    string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    return new InvalidOperationException(errMsg);
}

すべての例外メソッドを領域に囲み、クラスの最後に配置します。

長所:

  • メッセージをメソッドのコアロジックから除外します。
  • 各メッセージにロジック情報を追加できます(メソッドに引数を渡すことができます)

短所:

  • メソッドの混乱。場合によっては、例外を返すだけで、ビジネスロジックに実際には関係のない多くのメソッドが存在する可能性があります。
  • 他のクラスのメッセージは再利用できません

あなたが引用した短所は利点をはるかに上回っていると思います。
ネオタピル2013

@neontapir短所はすべて簡単に対処されます。補助メソッドにはIntentionRevealingNamesを指定し、いくつかにグループ化する必要があります#region #endregion(これにより、デフォルトでIDEから非表示になります)。それらが異なるクラスに適用できる場合は、クラスに配置しますinternal static ValidationUtility。ところで、C#プログラマの前で長い識別子名について文句を言うことはありません。
rwong 2015

しかし、地域について不満を言うでしょう。私の考えでは、もし自分が地域に訴えたいと思ったら、クラスには多すぎる責任があったでしょう。
ネオタピル2015

0

もう少し一般的なエラーを回避できる場合は、ソースの型を推測するパブリックスタティックジェネリックキャスト関数を作成できます。

public static I CastOrThrow<I,T>(T t, string source)
{
    if (t is I)
        return (I)t;

    string errMsg = string.Format(
          "Failed to complete {0}, because type: {1} could not be cast to type {2}.",
          source,
          typeof(T),
          typeof(I)
        );

    throw new InvalidOperationException(errMsg);
}


/// and then:

var interfaceInstance = SdkHelper.CastTo<IParameter>(passedObject, "Action constructor");

SdkHelper.RequireNotNull()入力の要件をチェックし、失敗した場合にのみスローするバリエーションが考えられます(考えます)。ただし、この例では、キャストを組み合わせて結果を生成すると、自己文書化されてコンパクトになります。

.net 4.5を使用している場合、コンパイラーに現在のメソッド/ファイルの名前をメソッドパラメーターとして挿入させる方法があります(CallerMemberAttibuteを参照)。しかし、SDKの場合、おそらく顧客に4.5への切り替えを要求することはできません。


キャストのこの特定の例ではなく、一般にスローされる例外(および例外内の情報の管理とコードの煩雑さ)の詳細です。
FriendlyGuy

0

ビジネスロジックエラー(必ずしも引数エラーなどである必要はありません)に対して私たちがしたいことは、すべての潜在的なタイプのエラーを定義する単一の列挙を持つことです。

/// <summary>
/// This enum is used to identify each business rule uniquely.
/// </summary>
public enum BusinessRuleId {

    /// <summary>
    /// Indicates that a valid body weight value of a patient is missing for dose calculation.
    /// </summary>
    [Display(Name = @"DoseCalculation_PatientBodyWeightMissing")]
    PatientBodyWeightMissingForDoseCalculation = 1,

    /// <summary>
    /// Indicates that a valid body height value of a patient is missing for dose calculation.
    /// </summary>
    [Display(Name = @"DoseCalculation_PatientBodyHeightMissing")]
    PatientBodyHeightMissingForDoseCalculation = 2,

    // ...
}

[Display(Name = "...")]属性は、エラーメッセージを翻訳するために使用するリソースファイルでキーを定義します。

また、このファイルは、特定のタイプのエラーがコードで生成されるすべての発生を見つけるための開始点として使用できます。

ビジネスルールのチェックは、違反したビジネスルールのリストを生成する専用のValidatorクラスに委任できます。

次に、カスタム例外タイプを使用して、違反したルールを転送します。

[Serializable]
public class BusinessLogicException : Exception {

    /// <summary>
    /// The Business Rule that was violated.
    /// </summary>
    public BusinessRuleId ViolatedBusinessRule { get; set; }

    /// <summary>
    /// Optional: additional parameters to be used to during generation of the error message.
    /// </summary>
    public string[] MessageParameters { get; set; }

    /// <summary>
    /// This exception indicates that a Business Rule has been violated. 
    /// </summary>
    public BusinessLogicException(BusinessRuleId violatedBusinessRule, params string[] messageParameters) {
        ViolatedBusinessRule = violatedBusinessRule;
        MessageParameters = messageParameters;
    }
}

バックエンドサービスコールは、違反したビジネスルールをユーザーが読み取り可能なエラーメッセージに変換する一般的なエラー処理コードにラップされます。

public object TryExecuteServiceAction(Action a) {
    try {
        return a();
    }
    catch (BusinessLogicException bex) {
        _logger.Error(GenerateErrorMessage(bex));
    }
}

public string GenerateErrorMessage(BusinessLogicException bex) {
    var translatedError = bex.ViolatedBusinessRule.ToTranslatedString();
    if (bex.MessageParameters != null) {
        translatedError = string.Format(translatedError, bex.MessageParameters);
    }
    return translatedError;
}

これToTranslatedString()は、属性enumからリソースキーを読み取り[Display]ResourceManagerこれらのキーを変換するために使用できる拡張メソッドです。各リソースキーの値は、のプレースホルダ含むことができstring.Format提供一致しますMessageParameters。resxファイルのエントリの例:

<data name="DoseCalculation_PatientBodyWeightMissing" xml:space="preserve">
    <value>The dose can not be calculated because the body weight observation for patient {0} is missing or not up to date.</value>
    <comment>{0} ... Patient name</comment>
</data>

使用例:

throw new BusinessLogicException(BusinessRuleId.PatientBodyWeightMissingForDoseCalculation, patient.Name);

このアプローチを使用すると、新しいタイプのエラーごとに新しい例外クラスを導入する必要なく、エラーメッセージの生成をエラーの生成から切り離すことができます。異なるフロントエンドが異なるメッセージを表示する必要がある場合、表示されるメッセージがユーザーの言語やロールなどに依存する必要がある場合に役立ちます。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.