【问题标题】:Java flow control by throwing exceptions通过抛出异常进行 Java 流控制
【发布时间】:2017-10-12 07:06:18
【问题描述】:

我有一个困扰我一段时间的问题,是关于更可取的流量控制方法。

我经常遇到一种情况,我必须根据 nullnot null 方法的返回值来决定要做什么

所以我有两个我知道的选择,如何处理它:

检查是否为空:

public class BasedOnNullFlowControl {

    public String process(String login) {
        String redirectUri = getRedirectUri(login);
        if (redirectUri != null) {
            return redirectUri;
        } else {
            return "Your login is taken";
        }
    }

    private String getRedirectUri(String login) {
        Optional<Login> loginFromDb = checkLoginExists(login);
        if (loginFromDb.isPresent()) {
            return null;
        } else {
            return "some-redirect-url";
        }
    }

    private Optional<Login> checkLoginExists(String login) {
        return Optional.empty(); //assume this value comes from some API
    }

    private class Login {}
}

或者异常中断流程:

public class ExceptionFlowControl {

    public String process(String login) {
        return getRedirectUri(login);
    }

    private String getRedirectUri(String login) {
        Optional<Login> loginFromDb = checkLoginExists(login);

        loginFromDb.ifPresent(l -> {
            throw new LoginExistsException();
        });
        return "some-redirect-url";
    }

    private Optional<Login> checkLoginExists(String login) {
        return Optional.empty();
    }

    private class LoginExistsException extends RuntimeException {
    }

    private class Login {}
}

我知道,应该只在特殊情况下使用异常,但我的情况并不特殊,第二个解决方案对我来说看起来更好,因为我可以添加一些异常处理程序(如在 Spring 中)并将其转换为一些不错的 Http最后的状态码。控制器方法process 没有被几十个空检查污染。

各位聪明人能否请教一下应该使用哪一种解决方案?或者也许还有第三个?

【问题讨论】:

  • 请注意,如果您使用的是if(Optional.isPresent()),那么您只是将null 替换为Optional,但不是以任何合理的方式。
  • 现在看看你的问题造成的混乱。
  • 谢谢大家的意见,现在我确信,我的问题没有单一的答案,可能一切都取决于谁在进行代码审查 :) 我只是想知道是否控制应用程序异常流动总是一种不好的做法,即使有时它看起来更简单、更干净
  • @nibsa 请考虑对您有帮助的回答和评论。也可以考虑选择一个答案。

标签: java exception control-flow


【解决方案1】:

异常方式允许代码部分之间的依赖较少,所以第一种方式很差。有了它,您需要从checkLoginExists()process 一直传递一个带有特殊含义的null 值(null == login taken)。

但是,抛出异常不是getRedirectUri() 的工作,应该在checkLoginExists() 中完成。我怀疑是否让getRedirectUri() 负责检查登录是否存在。更不用说使用nullOptional 只会给你一个成功/失败的二元概念。如果登录存在,但被锁定,所以你必须禁止登录以及创建一个具有相同名称的新用户。您可能希望重定向到其他页面以指示登录已锁定。

不过,这是基于意见的,高度依赖于具体情况,没有明确的规则可以遵循。

编辑(现在所有这些喧嚣都结束了):普遍接受的观点是,正常流控制不应该在例外情况下进行。然而,normal 的定义并不是那么容易定义的。你不会写这样的代码

int[] array = ...
int i = 0;
try {
    while(true) {
       array[i]++;
       i++;
    }
} catch(ArrayIndexOutOfBoundsException e) {
    // Loop finished
}

但是,如果您不使用上面这样一个非常简单的示例,它就会进入灰色区域。

另请注意,异常与应用程序错误不同。尝试通过验证所有内容来解决异常是不可能的(或者至少是不明智的)。如果我们考虑新用户尝试注册用户名的情况,并且我们希望防止重复的用户名。您可以检查重复并显示错误消息,但仍然有可能两个并发请求尝试注册相同的用户名。此时其中一个会出现异常,因为数据库将不允许重复(如果您的系统无论如何都是合理的)。

验证很重要,但没有什么真正的例外关于异常。您需要优雅地处理它们,所以如果您最终不得不处理异常,为什么还要麻烦验证。如果我们以我在 cmets 中写的内容为例,其中您需要防止重复和诅咒词作为用户名,您可能会想出类似的东西(假设这是在 getRedirectUri()

if(checkForCurseWords(username))
    return "login not allowed";

try {  // We can also omit the try-catch and let the exception bubble upwards
    createLogin(username);
} catch(DuplicateException e) {
    return "login exists";
} catch(Exception e) {
    return "problem with login";
}
return "main page";

【讨论】:

  • 我不同意:异常应该仅用于指示需要修复的应用程序错误,您应该在执行逻辑之前通过进行必要的验证来避免异常。
  • 验证失败是......正常的逻辑流程,显然不应该通过抛出异常来处理,当你写这个时,成千上万的小猫刚刚死去。
  • 验证异常是反模式,你应该知道的。
  • @KrzysztofCichocki “验证异常是反模式” - 谁说的?我打电话给BS。
  • @KrzysztofCichocki 我认为你有点过于依赖“避免像瘟疫这样的例外”。这不是使用异常来处理正常的逻辑流程。在这种情况下,正常的逻辑流程意味着创建了一个新用户,而异常用于指示无论出于何种原因都无法创建新用户。根据您的建议(以及我在答案中的情况),我们需要一种方法来检查现有登录名,另一种方法来检查该登录名是否被锁定,第三种方法来检查该登录名是否不允许(假设您可以'不要在登录时使用诅咒词)等等。
【解决方案2】:

首先,如果您使用可选项,您可以完全避免 null 检查:

public class BasedOnNullFlowControl {

    public String process(String login) {
        String redirectUri = 
        return getRedirectUri(login).orElse("Your login is taken");
    }

    private Optional<String> getRedirectUri(String login) {
        return checkLoginExists(login).map(ifExists -> "some-redirect-url");
    }

    private Optional<Login> checkLoginExists(String login) {
        return Optional.empty(); //assume this value comes from some API
    }

    private class Login {}
}

接下来,您的第二个版本只有未经检查的异常看起来不错。因此,这归结为已检查与未检查的异常。这是一种宗教问题。

几年前,我使用过的软件是上一代开发人员信奉未经检查的异常宗教的。比如,“无论如何,我们大多无法合理地处理异常,所以让我们抛出未经检查的异常,并在顶层设置一些例程来捕获它们并向用户显示错误信息对话框”。失败的壮观,重构它非常困难。代码就像一个雷区,你永远不知道会发生什么,什么不会发生。

在那之后,我完全转向了“检查异常宗教”。如果您遇到的情况可以更好地描述为编程错误(例如,传递了null,其中没有预期的 null 或访问必须存在的类路径资源),那么运行时错误是合适的。在其他情况下,建模并使用检查的异常。

【讨论】:

  • 我不同意:异常应该仅用于指示需要修复的应用程序错误,您应该在执行逻辑之前通过进行必要的验证来避免异常。
  • @KrzysztofCichocki 如果我编写了一个公共方法,但根据合同不接受nulls,我通常会使用Objects.requireNonNull 进行检查。如果有人使用null 调用该方法,他们将获得 NPE。在我的方法中,我无法验证“在执行逻辑之前”,这是无稽之谈,因为逻辑已经执行。不,特殊情况并不一定意味着应用程序中存在应该修复的错误。这意味着有一种情况你不能或决定不处理。
  • 在这种情况下,这应该在方法文档中描述,因此调用您的方法的人需要正确使用它(在调用您的方法之前检查他的值)。或者如果检查逻辑更复杂,你应该给他一个检查方法,就像在 File 类中完成的那样,例如file.exists()
  • @KrzysztofCichocki 验证输入参数并在输入错误时快速失败是一种对双方都有帮助的好习惯。用一些 Javadoc 代替这些检查并希望它们会被阅读和考虑并且没有人会犯错误是非常幼稚的。
【解决方案3】:

您将(很遗憾)永远不会得到此类问题的明确答案,因为这取决于很多因素,其中很多因素都是基于意见的。

我认为您的两个选项没有任何问题。对流控制使用异常在 Java 中被认为是不好的做法(但在其他一些语言中并非如此 - 例如 Python),但你说得有道理

controller方法进程不沾染几十个null 检查。

我给你一个技巧: 当您在两段代码之间犹豫时。问问自己,第一次看的人会更容易理解哪些代码。

在您的第二个解决方案中,很清楚如果会话已经存在会发生什么。六个月后,另一个开发人员可以查看您的代码,并通过搜索异常处理非常容易地找到您在这种情况下所做的事情。

鉴于我给你的技巧,我发现第二个选项更好(以我的拙见)。

希望这会有所帮助。

【讨论】:

  • 我已经给了他一个明确的答案。我不同意你的观点,因为抛出异常是最糟糕的解决方案。
【解决方案4】:

使用异常控制流总是坏主意,这里对其进行了广泛的解释: https://softwareengineering.stackexchange.com/questions/189222/are-exceptions-as-control-flow-considered-a-serious-antipattern-if-so-why

这里:

http://wiki.c2.com/?DontUseExceptionsForFlowControl

使用异常与正常逻辑的界限应该很清楚,所以异常应该只用于真正的错误,而不是用于正常应用程序逻辑可以服务的东西。

@lexicore 和@Kayaman,你现在可以给我投反对票,我不在乎。 并且...查看第二个链接以获取 真正的程序员 列表,他们说您不应该使用异常来进行正常的流程控制。 如果你的自我因此受到激怒,我真的很高兴。

【讨论】:

    【解决方案5】:

    这取决于:

    • 如果在您的逻辑中缺少值(或 null)是正常且预期的情况,那么您不应抛出异常,而应相应地处理它。考虑使用空对象模式 - 而不是返回空值返回一些表示“非值”对象的对象,然后您可以省略对空值的检查,因为您的逻辑应该相应地处理这样的空对象(通常您不需要甚至可以在逻辑中编写单行代码来为此类对象提供服务)。

    • 如果情况是您真的无法继续,您应该抛出异常。应使用异常来指示应用程序中的错误。如果您可以事先避免异常(例如,检查用户是否使用file.exists() 指定了正确的文件名),那么您应该这样做(除非它确实超出了正常的逻辑流程并且您无法继续)。

    在您的示例中,您确实不应该抛出异常,因为用户输入错误登录名是正常(和预期)情况,这应该由正常的逻辑流程处理。

    在您的特定情况下,为什么不直接返回指向带有“用户不存在”消息的某个页面的重定向 uri?

    【讨论】:

    • 所以按照你的建议,每当我们做文件IO时,我们需要检查文件是否存在(如果正在读取),我们是否有正确的权限,以及我们实际执行IO之前的任何其他情况,而不是通过异常处理它?
    • 您还误读了相关代码。这不是关于登录,而是关于创建新用户并防止重复登录。
    • 即使你在实际执行文件 IO 之前检查了所有内容,它仍然可能会失败(例如,在检查和 IO 之间删除文件)。
    • @KrzysztofCichocki 丢失的文件是“您的应用程序中的错误”吗?
    • OOME 也不例外。这是一个Error。不要混淆。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多