在搜索SO以获取与业务规则验证相关的错误处理方法时,我遇到的只是结构化异常处理的示例。
MSDN和许多其他声誉良好的开发资源非常清楚,例外不能用于处理例行错误案例。它们仅用于特殊情况和可能由于不正确而发生的意外错误由程序员(但不是用户)使用。在许多情况下,用户错误(例如留空的字段)很常见,而我们的程序应该期望的事情,因此不是例外而不是候选人使用例外。
QUOTE:
请记住使用该术语 编程中的异常必须要做 随着思想的异常 应该代表一个例外 条件。特殊情况,由 他们的本性,通常不会 发生;所以你的代码不应该抛出 例外是其日常生活的一部分 操作
不要向信号抛出异常 常见的事件。考虑一下 使用备用方法进行通信 调用者的发生 事件和保留例外 什么东西真的出来了 普通的事情发生了。
例如,正确使用:
private void DoSomething(string requiredParameter)
{
if (requiredParameter == null) throw new ArgumentExpcetion("requiredParameter cannot be null");
// Remainder of method body...
}
使用不当:
// Renames item to a name supplied by the user. Name must begin with an "F".
public void RenameItem(string newName)
{
// Items must have names that begin with "F"
if (!newName.StartsWith("F")) throw new RenameException("New name must begin with /"F/"");
// Remainder of method body...
}
在上述情况下,根据最佳实践,最好将错误传递给UI而不涉及/要求.NET的异常处理机制。
使用上面的相同示例,假设有人需要对项目强制执行一组命名规则。什么方法最好?
让方法返回a 枚举结果? RenameResult.Success, RenameResult.TooShort, RenameResult.TooLong, RenameResult.InvalidCharacters等。
在控制器类中使用事件 向UI类报告?用户界面调用 控制器的RenameItem方法,然后处理一个 控制器引发的AfterRename事件 作为事件args的一部分重命名状态?
控制类直接引用 并从UI类调用一个方法 处理错误,例如ReportError(字符串文本)。
别的什么......?
基本上,我想知道如何在可能不是Form类本身的类中执行复杂验证,并将错误传递回Form类进行显示 - 但我不想让异常处理涉及它应该在哪里不被使用(即使它看起来容易得多!)
根据对问题的回答,我觉得我必须以更具体的方式陈述问题:
UI =用户界面,BLL =业务逻辑层(在这种情况下,只是一个不同的类)
由于用户输入无效值是常规的,因此不应使用例外。没有例外的正确方法是什么?
答案 0 :(得分:4)
我认为您对预期信息的印象错误。这是我昨天从current edition of Visual Studio magazine(第19卷,第8期)开始的一个很好的引用。
要么会员履行合同,要么退出。期。没有中间立场。没有返回代码,有时没有返回代码,有时却没有。
应该谨慎使用异常,因为创建和抛出它们很昂贵 - 但是,它们是.NET框架通知客户端(我的意思是任何调用组件)错误的方式。
答案 1 :(得分:4)
我假设您正在创建自己的业务规则验证引擎,因为您没有提到您正在使用的那个。
我会使用异常,但我不会抛弃它们。您显然需要在某处累积评估状态 - 记录特定规则失败的事实,我将存储描述失败的Exception实例。这是因为:
Message
属性,并且可以具有其他属性,以机器可读的形式记录异常的详细信息。FormatException
。您可以捕获该异常并将其添加到列表中。事实上,本月的MSDN杂志有一篇文章提到了.NET 4.0中新的AggregateException
类,它是一组在特定上下文中发生的异常。
由于您使用的是Windows窗体,因此应使用内置机制进行验证:Validating
事件和ErrorProvider
组件。
答案 2 :(得分:3)
您提供的示例是UI验证输入。
因此,一种好的方法是将验证与操作分开。 WinForms有一个内置的验证系统,但原则上它的工作原理如下:
ValidationResult v = ValidateName(string newName);
if (v == ValidationResult.NameOk)
SetName(newName);
else
ReportErrorAndAskUserToRetry(...);
此外,您可以在SetName方法中应用验证,以确保已检查有效性:
public void SetName(string newName)
{
if (ValidateName(newName) != ValidationResult.NameOk)
throw new InvalidOperationException("name has not been correctly validated");
name = newName;
}
(请注意,这可能不是性能的最佳方法,但是在对UI输入应用简单验证检查的情况下,验证两次具有任何意义的可能性不大。或者,上述检查可能是完全作为一个仅调试的断言检查,以捕获程序员在没有首先验证输入的情况下调用该方法的任何尝试。一旦你知道所有调用者都遵守他们的契约,通常根本不需要发布运行时检查)
引用另一个答案:
Either a member fulfills its contract or it throws an exception. Period.
这件事遗漏的是:合同是什么?在“契约”中声明方法返回状态值是完全合理的。例如File.Exists()返回状态代码,而不是异常,因为这是它的合同。
但是,你的例子不同。在其中,您实际上执行两个单独的操作:验证和存储。如果SetName可以返回状态代码或设置名称,它会尝试在一个中执行两个任务,这意味着调用者永远不知道它将展示哪种行为,并且必须具有特殊的案例处理对于那些情况。但是,如果将SetName拆分为单独的验证和存储步骤,则StoreName的合同可以是您传入有效输入(由ValidateName传递),如果不符合此合同,则会抛出异常。因为每个方法只做一件事而且只做一件事,合同非常明确,而且应该抛出异常是显而易见的。
答案 3 :(得分:2)
我同意Henk的建议。
传统上,“通过/失败”操作是作为具有整数或bool返回类型的函数实现的,该类型将指定调用的结果。但是,有些人反对这一点,指出“一个函数或方法应该执行一个动作或返回一个值,但不能同时返回两个值。”换句话说,返回值的类memeber也不应该是更改对象状态的类memeber。
我找到了在生成广告的类中添加 .HasErrors / .IsValid 和 .Errors 属性的最佳解决方案错误。前两个属性允许客户端类测试是否存在错误,如果需要,还可以读取.Errors属性并报告包含的一个或所有错误。然后,每个方法都必须了解这些属性并适当地管理错误状态。然后,可以将这些属性转换为各种业务规则层外观类可以合并的IErrorReporting接口。
答案 4 :(得分:1)
在我看来,如果有疑问,请在业务规则验证中抛出异常。我知道这有点违反直觉,我可能会因此而受到抨击,但我坚持认为,因为什么是例行公事,什么不是取决于具体情况,是商业决策,而不是编程决策。
例如,如果您有WPF应用程序和WCF服务使用的业务域类,则在WPF应用程序中无效的字段输入可能是常规的,但是当在域中使用域对象时,这将是灾难性的。您正在处理来自其他应用程序的服务请求的WCF情况。
我想了很久很努力,想出了这个解决方案。对域类执行以下操作:
答案 5 :(得分:0)
例外只是:一种处理特殊情况的方法,这种情况通常不会在您的应用程序中发生。提供的两个示例都是如何正确使用异常的合理示例。在这两个实例中,他们都确定已经调用了一个不允许发生的操作,并且对于应用程序的正常流程来说是例外。
误解是第二个错误,重命名方法,是唯一机制来检测重命名错误。永远不应将异常用作将消息传递到用户界面的机制。在这种情况下,您将有一些逻辑检查为重命名指定的名称在UI验证中的某处有效。此验证将使异常永远不会成为正常流程的一部分。
可以阻止“坏事”的发生,例如,它们可以作为API调用的最后防线,以确保错误停止并且只能发生合法行为。它们是程序员级别的错误,应该只表示发生以下情况之一:
它们不应该是防止用户错误的唯一防线。用户需要的反馈和关注远远超过异常提供的范围,并且例行 尝试做一些超出预期应用程序流程的事情。
答案 6 :(得分:0)
我认为我几乎已经确信,抛出异常实际上是验证类型操作(尤其是聚合类型操作)的最佳做法。
以一个示例为例,更新数据库中的一条记录。这是许多可能个别失败的操作的总和。
例如。
如果我们想避免出现异常,那么我们可能想使用可以保存成功或错误状态的对象:
public class Result<T> {
public T Value { get; }
public string Error { get; }
public Result(T value) => Value = value;
public Result(string error) => Error = error;
public bool HasError() => Error != null;
public bool Ok() => !HasError();
}
现在,我们可以在一些辅助方法中使用此对象:
public Result<Record> FindRecord(int id) {
var record = Records.Find(id);
if (record == null) return new Result<Record>("Record not found");
return new Result<Record>(record);
}
public Results<bool> RecordIsValid(Record record) {
var validator = new Validator<Record>(record);
if (validator.IsValid()) return new Result<bool>(true);
return new Result<bool>(validator.ErrorMessages);
}
public Result<bool> UpdateRecord(Record record) {
try {
Records.Update(record);
Records.Save();
return new Result<bool>(true);
}
catch (DbUpdateException e) {
new Result<bool>(e.Message);
}
}
现在将我们的聚合方法结合在一起:
public Result<bool> UpdateRecord(int id, Record record) {
if (id != record.ID) return new Result<bool>("ID of record cannot be modified");
var dbRecordResults = FindRecord(id);
if (dbRecordResults.HasError())
return new Result<bool>(dbRecordResults.Error);
var validationResults = RecordIsValid(record);
if (validationResults.HasError())
return validationResults;
var updateResult = UpdateRecord(record);
return updateResult;
}
哇!真是一团糟!
我们可以更进一步。我们可以创建Result<T>
的子类来指示特定的错误类型:
public class ValidationError : Result<bool> {
public ValidationError(string validationError) : base(validationError) {}
}
public class RecordNotFound: Result<Record> {
public RecordNotFound(int id) : base($"Record not found: ID = {id}") {}
}
public class DbUpdateError : Result<bool> {
public DbUpdateError(DbUpdateException e) : base(e.Message) {}
}
然后我们可以测试特定的错误情况:
var result = UpdateRecord(id, record);
if (result is RecordNotFound) return NotFound();
if (result is ValidationError) return UnprocessableEntity(result.Error);
if (result.HasError()) return UnprocessableEntity(result.Error);
return Ok(result.Value);
但是,在上面的示例中,result is RecordNotFound
总是返回false,因为它是Result<Record>
,而UpdateRecord(id, record)
返回Result<bool>
。
一些积极的方面:
*它基本上可以工作
*避免异常
*当事情失败时它会返回好消息
* Result<T>
类可以根据需要复杂。例如,在验证错误消息的情况下,也许它可以处理一系列错误消息。
* Result<T>
的子类可用于指示常见错误
底片:
*有些转换问题中T
可能有所不同。例如。 Result<T>
和Result<Record>
*这些方法现在正在执行多种操作,错误处理以及它们应该执行的操作
*非常冗长
*诸如UpdateRecord(int, Record)
之类的聚合方法现在需要让他们自己关注调用的方法的结果。
现在使用例外...
public class ValidationException : Exception {
public ValidationException(string message) : base(message) {}
}
public class RecordNotFoundException : Exception {
public RecordNotFoundException (int id) : base($"Record not found: ID = {id}") {}
}
public class IdMisMatchException : Exception {
public IdMisMatchException(string message) : base(message) {}
}
public Record FindRecord(int id) {
var record = Records.Find(id);
if (record == null) throw new RecordNotFoundException("Record not found");
return record;
}
public bool RecordIsValid(Record record) {
var validator = new Validator<Record>(record);
if (!validator.IsValid()) throw new ValidationException(validator.ErrorMessages)
return true;
}
public bool UpdateRecord(Record record) {
Records.Update(record);
Records.Save();
return true;
}
public bool UpdateRecord(int id, Record record) {
if (id != record.ID) throw new IdMisMatchException("ID of record cannot be modified");
FindRecord(id);
RecordIsValid(record);
UpdateRecord(record);
return true;
}
然后在控制器操作中:
try {
UpdateRecord(id, record)
return Ok(record);
}
catch (RecordNotFoundException) { return NotFound(); }
// ...
该代码更加简单... 每个方法要么起作用,要么引发特定的异常子类... 没有检查方法是否成功的检查。 没有类型转换带有异常结果... 您可以轻松添加一个应用程序范围的异常处理程序,该处理程序根据异常类型返回正确的响应和状态代码... 在控制流中使用异常有很多优点……
我不确定负面因素是什么...人们说他们像GOTOs ...不是很确定为什么这很糟糕...他们还说性能不好...但是那又是什么呢?与进行的数据库调用相比,这有何不同?我不确定这些负面因素是否真的是正当理由。