平衡信息异常和干净代码的好方法是什么?


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
恕我直言,您同事的解决方案是一个很好的解决方案,我想如果您真的有很多此类额外方法,则可以至少重用其中一些方法。只要您的方法命名合理,就可以使用很多小方法,只要它们可以创建程序的易于理解的构造块-此处似乎就是这种情况。
布朗

@DocBrown-是的,我喜欢这个主意(在下面的答案中加上优点/缺点),但是对于智能感知和潜在的方法数量而言,它也开始看起来很混乱。
FriendlyGuy

1
想法:不要使消息成为所有异常细节载体。使用Exception.Data属性,“挑剔”异常捕获,调用代码捕获并添加其自己的上下文以及捕获的调用堆栈的组合都构成了信息,这些信息应允许少得多的冗长消息。最后System.Reflection.MethodBase看起来很有希望提供详细信息以传递给您的“异常构造”方法。
radarbob

@radarbob您是否建议例外情况太冗长?也许我们使它变得太像日志记录了。
FriendlyGuy

1
@MackleChan,我读到这里的范例是将信息放入一条消息中,该消息试图准确地说出发生了什么,在语法上一定是正确的,并且假装AI:“ ..通过空构造函数起作用,但是...”真?我内部的法证编码器将其视为由于堆栈跟踪丢失而重新抛出的常见错误以及对的不了解的演变结果Exception.Data。重点应该是捕获遥测。在这里进行重构很好,但是遗漏了问题。
2013年

Answers:


10

为什么没有专门的异常类?

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

这样会将格式和细节推到异常本身,而使主类变得整洁。


1
优点:非常干净且井井有条,为每个异常提供有意义的名称。缺点:可能有很多额外的异常类。
FriendlyGuy

如果重要的话,您可以使用数量有限的异常类,将异常起源的类作为参数传递,并在更通用的异常类内部进行巨大的转换。但这有点淡淡的味道-不确定我会去那里。
ptyx

闻起来真的很糟糕(类的异常紧密结合)我想很难在可自定义的异常消息与异常数量之间取得平衡。
FriendlyGuy

7

Microsoft似乎(通过查看.NET源代码)有时会使用资源/环境字符串。例如ParseDecimal

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

优点:

  • 集中例外消息,允许重复使用
  • 使异常消息(可以说与代码无关)远离方法的逻辑
  • 抛出的异常类型很明显
  • 消息可以本地化

缺点:

  • 如果更改了一条异常消息,则它们都将改变
  • 异常消息对于抛出异常的代码不是那么容易获得。
  • 消息是静态的,不包含有关错误值的信息。如果要格式化,则代码中的内容会更加混乱。

2
您留下了巨大的好处:异常文本的本地化

@ 17of26-好点,添加它。
FriendlyGuy

我支持您的回答,但是以这种方式发出的错误消息是“静态的”。您不能像OP在他的代码中所做的那样向它们添加修饰符。因此,您实际上已经忽略了他的某些功能。
罗伯特·哈维

@RobertHarvey-我将其添加为缺点。这确实可以解释为什么内置异常永远不会真正为您提供本地信息。另外,仅供参考,我是OP(我知道此解决方案,但我想知道其他人是否有更好的解决方案)。
FriendlyGuy

6
@ 17of26作为开发人员,我满怀热情地讨厌本地化异常。我每次都必须取消本地化,例如。谷歌的解决方案。
Konrad Morawski 2013年

2

对于公共SDK场景,我会强烈考虑使用Microsoft代码合同,因为它们会提供信息错误,静态检查,并且您还可以生成文档以添加到XML文档和Sandcastle生成的帮助文件中。所有付费的Visual Studio版本均支持该功能。

另一个好处是,如果您的客户正在使用C#,则他们甚至可以在运行代码之前利用您的代码合同参考程序集来检测潜在的问题。

代码合同的完整文档在这里


2

我使用的技术是合并验证并将其完全外包给实用程序函数。

最重要的好处是,它在业务逻辑中简化为单一语言

我敢打赌,除非进一步减少,否则您将无法做得更好-从业务逻辑中消除所有参数验证和对象状态保护,仅保留特殊的操作条件。

当然,有很多方法可以实现-强类型语言,“任何时候都不允许有无效对象”设计,按合同设计等。

例:

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);
}

将所有异常方法封装到区域中,并将它们放在类的末尾。

优点:

  • 将消息置于方法的核心逻辑之外
  • 允许向每条消息添加逻辑信息(您可以将参数传递给方法)

缺点:

  • 方法混乱。潜在地,您可能有很多只返回异常且与业务逻辑没有真正关系的方法。
  • 无法重用其他类别中的消息

我认为您引用的弊端远远超过了优点。
neontapir 2013年

@neontapir的缺点都可以轻松解决。辅助方法应该被赋予IntentionRevealingNames并被分组为一些#region #endregion(默认情况下,它们会从IDE中隐藏),如果它们适用于不同的类,则将它们放入一个internal static ValidationUtility类中。顺便说一句,永远不要在C#程序员面前抱怨长标识符名称。
rwong 2015年

我会抱怨地区。在我看来,如果您发现自己想诉诸地区,那么全班可能承担了太多责任。
neontapir 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类,该类产生违反业务规则的列表。

然后,我们使用自定义的Exception类型来传输违反的规则:

[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);

使用这种方法,您可以将错误消息的生成与错误的生成分离,而无需为每种新的错误类型引入新的异常类。如果不同的前端应显示不同的消息,或者所显示的消息应取决于用户语言和/或角色等,则很有用。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.