什么是一个好的“错误检查”模式(Java)?

18

我将解释一下输入错误检查的含义。

假设你有一个函数 doSomething(x)

如果该函数成功完成,则 doSomething 执行某些操作并返回空值。 然而,如果出现错误,我希望能够得到通知。 这就是我所说的错误检查。

总的来说,我正在寻找最佳检查错误的方法。 我想到了以下几种解决方案,每种方案都存在潜在问题。

  1. 标志错误检查。 如果 doSomething(x) 成功完成则返回 null。否则,它会返回布尔值或错误字符串。 问题:副作用。

  2. 抛出异常。 如果 doSomething(x) 遇到错误,则抛出异常。 问题:如果您仅对参数进行错误检查,则抛出 IllegalArgumentException 似乎不合适。

  3. 在函数调用之前验证输入。 如果错误检查仅针对函数的参数,则可以在调用 doSomething(x) 函数之前调用验证器函数。 问题:如果类的客户端忘记在调用 doSomething(x) 之前调用验证器函数会怎么样?

我经常遇到这个问题,如果您能提供任何帮助或指引方向,将不胜感激。


通知是其中的一部分,但是你要如何处理“错误”(例如第3个建议)?并且,如果您选择此解决方案,异常会被引发多少次? - LB40
9个回答

7

抛出异常是最好的方式。

如果您只对参数执行错误检查,则抛出IllegalArgumentException似乎不合适。

为什么?这就是这个异常的目的。


同意,为什么会不合适呢?这是一个特殊的情况(我假设),而且错误信息足够具体,你的客户端代码可以对其采取有意义的措施。 - lucas
我应该澄清一下,我所考虑的参数错误检查是针对用户输入的。根据Effective Java的说法:“使用运行时异常来指示编程错误”。正如aiobee在下面提到的那样,运行时异常是用于错误而不是无效的用户输入。 - volni
2
你不必让运行时异常一直传递到用户界面。你可以通过调用函数捕获它,而不是重新抛出它,打印一个用户友好的消息。例如,tan(90 deg) 是未定义的,该函数应该抛出异常,但是调用函数应该将捕获的异常转换为用户的消息。 - FrustratedWithFormsDesigner
1
我理解你的观点。但是人们经常寻找IllegalArgumentException的已检查异常版本吗?他们会自己创建吗?它未经检查的部分让我感到困扰。 - volni
1
@支持,如果方法的输入应该已经过净化处理,但实际上没有进行净化处理,则确实表明存在编程错误。你不会直接将未经净化处理的用户输入传递到无关的方法中了吧? - Mike Daniels
显示剩余2条评论

5
  1. 标志错误检查

在某些情况下,这是适当的,具体取决于您对“错误”的定义。

来自API的示例:如果您尝试将一个对象添加到已经包含与新对象相等的另一个对象的Set中,则add方法会有点“失败”,并通过返回false来指示此情况。 (请注意,我们处于一个技术上甚至不是“错误”的级别!)

2.抛出异常

这是默认选项。

问题是,您应该选择可检查的异常(需要throws声明或try/catch子句)还是未经检查的异常(扩展了RuntimeException的异常)。 这里有一些经验法则。

来自Java实践 -> 可检查 versus 未检查的异常:

  • 未检查的异常:表示程序中的缺陷(错误)-通常是传递给非私有方法的无效参数。

  • 可检查的异常:表示程序外部不受直接控制的区域中的无效条件(无效的用户输入、数据库问题、网络中断、不存在的文件)

请注意,IllegalArgumentException是一个未经检查的异常,非常适合在参数不正确时抛出。

如果要抛出受检异常,则可以A)通过扩展Exception自定义异常,B)使用一些现有的受检异常或C)将运行时异常“链接”到例如IOException中:throw new IOException(new IllegalArgumentException("原因在这里..."));

3.在函数调用前验证输入

依靠客户端在调用之前对其参数进行净化/检查似乎对我来说是一个坏主意。


4
你的第二个建议("抛出异常")是最好的选择。其他两个选项依赖于调用者在方法调用之前("在函数调用之前验证输入")或之后("标记错误检查")执行某些操作。无论哪种方式,这个额外的任务都不是由编译器强制执行的,因此调用函数的人没有被强制调用“额外的事情”,因此问题直到运行时才能被发现。
至于“抛出异常”和你提出的“问题”,那么答案是为代码抛出适当的异常类型。如果输入参数无效,则抛出一个InvalidArgumentException(因为这是适当的错误)。如果异常是针对功能(例如不能打开网络连接),则使用另一种异常类型或创建自己的异常类型。

我的声望太低了,无法在其他人的答案下添加评论,但我同意“stacker”关于用户界面的第一个要点。 - Phil

2
我同意使用抛出异常的方式。我想再添加一种选项,即代理模式。这样你的代码就保持了相当的内聚性——验证在一个地方,业务逻辑在另一个地方。如果你有一组需要验证的大量调用,这是有意义的。
创建一个代理来处理验证。它在验证之后将所有调用委托给实际的业务逻辑接口实现,否则它会抛出异常,如果某些内容无法验证的话。

1

我通常根据接口类型来决定使用哪种方法。

  • 用户界面(GUI):在调用业务方法之前,我会进行验证,因为用户想知道出了什么问题。

  • 在组件或系统之间的技术接口上,接口应该已经经过测试并正常工作,在这种情况下我会抛出异常。


你应该同时进行客户端和服务器端验证(不要相信客户端!) - Pascal Thivent

1

异常处理是解决问题的途径。通过正确实现异常抛出/处理,可以缓解您所述的异常问题。利用异常优势,在需要最低级别验证参数并且验证失败时抛出异常。这样可以避免在调用堆栈的多个级别上进行冗余有效性检查。在底部抛出异常,让堆栈展开到适当的位置以处理错误。


1
你选择的方法取决于情况,它们并不是互相排斥的,因此你可以在同一个解决方案中混合使用它们(尽管是否明智取决于你的具体情况)。
  1. 如果您想要一个非常简单的处理错误的方法,请选择此方法。在调用函数可以接受被调用函数返回的任何值的情况下,这种方法可能是可以接受的。在某些情况下,业务逻辑可能会指定这是一个可以接受的选择,例如在无法正确定位资源或服务器未响应时返回特定的消息字符串。通常情况下,我不太使用此技术,也很少在Java中看到此技术,因为异常是更好的错误处理机制。

  2. 当您的函数遇到未定义的行为时,请抛出异常。如果您有一个只能对正整数进行操作的数学函数,并且有人传递了-1,则应该抛出InvalidArguementException异常。如果您的函数给出了数据库中产品的ID,但是查询无法找到该产品,则可以抛出自定义的ProductNotFound异常。

  3. 验证输入是个好主意,我认为应该由被调用函数而不是调用者来完成,除非调用者可以通过验证输入来避免被调用者引发异常。如果您使用支持设计合同的语言,则验证输入将作为函数的前提条件。

我通常使用#2和#3。我已经有一段时间没有编写带有错误标志的代码了。唯一的例外可能是返回枚举的函数,其中一个可能的值表示错误代码。这更多地受业务规则的驱动而不是其他任何因素。

总的来说,尽量保持简单。


0

抛出自定义的已检查异常。

 doSomething(WithX x ) throws BusinessRuleViolatedException 

异常用于流程控制是一种反模式:http://c2.com/cgi/wiki?DontUseExceptionsForFlowControl - LaFayette

0

输入验证非常复杂,原帖中提出的三种方法都是必需的,有时还需要更多。当输入超出业务逻辑的范围、数据损坏或无法读取时,异常是合适的。

如果您要检查的标志超过一两个,则标志检查很快就会成为反模式,并且可以用访问者模式的稍微专门化的版本进行替换。我不知道这个具体模式的确切名称,但我会非正式地称它为“验证器列表模式”,下面将详细描述它。

尽早检查输入并快速失败通常是好的,但并非总是可能的。通常存在大量的输入验证,来自您控制范围之外的所有输入都应视为敌对并需要验证。良好的程序设计和架构将有助于明确何时需要进行此操作。

“验证器列表模式”

作为一个例子,让我们首先在代码中描述“验证标志”反模式,然后将其转换为“验证器列表”模式。

public Optional<String> checkForErrorsUsingFlags( 
       ObjectToCheck objToCheck ) {
  // the small series of checks and if statements represent the
  // anti-pattern. Hard to test and many other problems crop up.
  String errMsg = checkForError1( objToCheck );
  if(errMsg != null ) {
    return Optional.of(errMsg);
  }
  errMsg = checkForError2( objToCheck );
  if(errMsg != null ) {
    return Optional.of(errMsg);
  }
  return Optional.empty();
}

/**** client usage  ****/
ObjectToCheck obj = doSomethingToReadInput(obj);
Optional<String> error = checkForErrors( obj);
if (error.isPresent()) {
  // invalid input, throw object away and request input again
} else {
  // do stuff, we have a valid input
}

要修复问题,首先创建一个通用接口来表示单个验证器。然后将每个检查转换为验证器实例。最后创建一个验证器列表并将其传递给验证器代码。
/** The common validator interface each validator will use */
private interface MyValidator {
  public boolean isValid(ObjectToCheck obj);
  public String getErrorMessage(ObjectToCheck obj);
}

 // this method should look familiar to the above, now we 
 // have a list of validators as an additional parameter
public Optional<String> checkForErrors( ObjectToCheck objToCheck,
     List<MyValidator> validators ) {
  for(MyValidator validator : validators ) {
    if (!validator.isValid(objToCheck)) {
      String errMsg = validator.getErrorMessage(objToCheck);
      return Optional.of(errMsg);
    }
  }
  return Optional.empty();
}

/****** client usage  *****/
// now in this pattern, the client controls when the validators
// are created, and which ones are used.
MyValidator validator1 = new MyValidator() {
  @Override
  public boolean isValid(ObjectToCheck obj) {
    return checkForError1( objToCheck ) != null;
  }
  @Override
  public boolean getErrorMessage(ObjectToCheck obj) {
    return checkForError1( objToCheck );
  }
}
// note: above we call checkForError1 twice, not optimal.
// typical in real examples this can be avoided,
// and the error message generation logic split from the detection
// logic often simplifies things.
MyValidator validator2 = new MyValidator() { ... }

List<MyValidator> validators = 
  ImmutableList.of( validator1, validator2);
Optional<String> error = checkForErrors(objToCheck, validators);
if (error.isPresent()) {
    // invalid input, throw object away and request input again
} else {
    // do stuff, we have a valid input
}

现在开始进行测试,创建一系列模拟验证器并检查每个验证器是否已调用其验证方法。您可以存根验证器结果并确保采取正确的行为。然后,您还可以单独访问每个验证器,以便可以单独测试它们。祝好运 - 希望这有所帮助,愉快地编码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接