在这里抛出异常是一种反模式吗?


33

代码审查后,我刚刚讨论了设计选择。我想知道您的意见是什么。

有一个此类Preferences,它是键-值对的存储桶。空值是合法的(这很重要)。我们希望某些值可能尚未保存,并且我们希望通过在请求时使用预定义的默认值对其进行初始化来自动处理这些情况。

讨论的解决方案使用以下模式(注意:显然,这不是实际的代码-出于说明目的而进行了简化):

public class Preferences {
    // null values are legal
    private Map<String, String> valuesFromDatabase;

    private static Map<String, String> defaultValues;

    class KeyNotFoundException extends Exception {
    }

    public String getByKey(String key) {
        try {
            return getValueByKey(key);
        } catch (KeyNotFoundException e) {
            String defaultValue = defaultValues.get(key);
            valuesFromDatabase.put(key, defaultvalue);
            return defaultValue;
        }
    }

    private String getValueByKey(String key) throws KeyNotFoundException {
        if (valuesFromDatabase.containsKey(key)) {
            return valuesFromDatabase.get(key);
        } else {
            throw new KeyNotFoundException();
        }
    }
}

它被批评为控制流的反模式滥用异常KeyNotFoundException-仅针对该用例而生-永远不会超出此类范围。

从本质上讲,这是两种使用fetch进行获取信息的方法。

数据库中不存在的密钥并不令人担忧或例外-我们希望每当添加新的首选项设置时都会发生这种情况,因此,如果需要,可以使用默认值优雅地对其进行初始化的机制。

与此相反的是getValueByKey-私有方法-因为现在定义具有告知公众对方法没有自然的方式,这两种价值,关键是否在那里。(如果不是,则必须添加它,以便可以更新值)。

返回值null是模棱两可的,因为这null是完全合法的值,因此无法确定它是否意味着密钥不存在,或者是否存在null

getValueByKey将必须返回某种a Tuple<Boolean, String>,如果密钥已经存在,则将bool设置为true,以便我们可以区分(true, null)(false, null)。(out可以在C#中使用参数,但这是Java)。

这是更好的选择吗?是的,您必须定义一些一次性使用的类以达到的效果Tuple<Boolean, String>,但随后我们将摆脱KeyNotFoundException,从而实现了这种平衡。我们也避免了处理异常的开销,尽管它在实际意义上并不重要-无需考虑性能方面的考虑,它是客户端应用程序,也不像每秒会检索用户偏好数百万次。

这种方法的一种变体可以使用Guava Optional<String>(在整个项目中已经使用了Guava)而不是一些自定义方法Tuple<Boolean, String>,然后我们就可以区分Optional.<String>absent()“适当”了null。但是,由于易于理解的原因,它仍然让人感到有些呆滞-引入两个“空”级别似乎首先滥用了创建Optionals 背后的概念。

另一个选择是显式检查密钥是否存在(添加boolean containsKey(String key)方法并getValueByKey仅在我们已经断言它存在的情况下调用)。

最后,也可以内联私有方法,但是实际方法getByKey比我的代码示例复杂一些,因此内联会使它看起来很讨厌。

我可能会在这里分开,但我很好奇在这种情况下您会押宝于最佳实践。我在Oracle或Google的风格指南中找不到答案。

使用代码示例中的异常是一种反模式,还是因为替代方法不是很干净?如果是,在什么情况下可以?反之亦然?


1
@ GlenH7接受(且投票最高)的答案总结为:“在它们使错误处理更容易且代码混乱更少的情况下,应使用它们[exceptions]”。我想我想问的是,我列出的替代方案是否应被视为“较少的代码混乱”。
Konrad Morawski 2015年

1
我撤消了我的VTC-您要的是与我建议的内容有关但又与我有所不同的内容。

3
我认为这个问题在getValueByKey公开场合也会变得更加有趣。
布朗

2
供将来参考,您做了正确的事。因为它是示例代码,所以本来不在Code Review上讨论。
RubberDuck

1
只是为了达到目的---这是完全惯用的Python,并且(我认为)将是使用该语言处理此问题的首选方式。但是,显然,不同的语言具有不同的约定。
帕特里克·柯林斯

Answers:


75

是的,您的同事是对的:那是错误的代码。如果错误可以在本地处理,则应立即处理。不应引发异常,然后立即处理异常。

这比您的版本干净得多(该getValueByKey()方法已删除):

public String getByKey(String key) {
    if (valuesFromDatabase.containsKey(key)) {
        return valuesFromDatabase.get(key);
    } else {
        String defaultValue = defaultValues.get(key);
        valuesFromDatabase.put(key, defaultvalue);
        return defaultValue;
    }
}

仅当您不知道如何在本地解决错误时,才应引发异常。


14
感谢您的回答。作为记录,我是这个同事(我不喜欢原始代码),我只是中立地回答了这个问题,以便不加载它:)
Konrad Morawski 2015年

59
精神错乱的第一个迹象:您开始自言自语。
罗伯特·哈维

1
@BЈовић:好吧,在那种情况下,我认为还可以,但是如果您需要两种变体,一种方法是在不自动创建丢失键的情况下检索值,而另一种方法是?那么您认为最干净的(和干的)替代方案是什么?
Doc Brown

3
@RobertHarvey :)其他人编写了这段代码,实际上是一个单独的人,我是审稿人
Konrad Morawski

1
@Alvaro,经常使用异常不是问题。无论语言如何,严重使用异常都是一个问题。
kdbanman 2015年

11

我不会将这种异常的使用称为反模式,不是解决复杂结果传递问题的最佳解决方案。

最好的解决方案(假设您仍使用Java 7)是使用Guava的Optional;我不同意在这种情况下使用它会有点黑。在我看来,基于番石榴对Optional的扩展解释,这是何时使用它的完美示例。您要区分“找不到值”和“找到空值”。


1
我不认为Optional可以保留null。Optional.of()只接受非空引用,Optional.fromNullable()并将空视为“不存在值”。
纳文

1
@Navin不能保留null,但是您可以使用Optional.absent()。因此,Optional.fromNullable(string)等于Optional.<String>absent()是否string为null或Optional.of(string)不是。
Konrad Morawski 2015年

@KonradMorawski我认为OP中的问题是,如果不抛出异常,您将无法区分空字符串和不存在的字符串。Optional.absent()涵盖了这些情况之一。您如何代表另一个人?
纳文

@Navin与实际的null
Konrad Morawski

4
@KonradMorawski:听起来真是个坏主意。我几乎想不出一种更清晰的方式来表达“此方法永不返回null!”。而不是将它声明为返回Optional<...>。。。
ruakh 2015年

8

由于没有性能方面的考虑,并且它是一个实现细节,因此最终选择哪个解决方案都没有关系。但是我必须同意这是不好的风格。缺席的关键是您将要发生的事情,您甚至最多只能处理一次调用堆栈,这是异常处理最有用的地方。

元组方法有点笨拙,因为当布尔值为假时,第二个字段是无意义的。事先检查密钥是否存在是很愚蠢的,因为地图两次查找了密钥。(嗯,您已经在做,所以在这种情况下是三次。)该Optional解决方案非常适合该问题。这似乎有点讽刺意味,以存储nullOptional,但如果这是用户想要做什么,你不能说没有。

正如Mike在评论中指出的那样,这是有问题的。Guava和Java 8都Optional不允许存储null。因此,您需要自己动手制作(虽然简单易行),但涉及大量样板,因此对于只在内部使用一次的内容来说可能会过分杀伤力。您也可以将地图的类型更改为Map<String, Optional<String>>,但处理Optional<Optional<String>>s会很麻烦。

一个合理的折衷办法可能是保留异常,但要承认其作为“替代回报”的作用。创建一个禁用堆栈跟踪的新检查异常,并抛出一个预先创建的实例,您可以将该实例保存在静态变量中。这很便宜- 可能和本地分支一样便宜 -并且您不能忘记处理它,因此缺少堆栈跟踪不是问题。


1
好吧,实际上,它只是Map<String, String>在数据库表级别上,因为values列必须是一种类型。但是,有一个包装器DAO层对每个键值对进行强类型化。但是为了清楚起见,我省略了这一点。(当然,您可以有一个带有n列的单行表,但是随后添加每个新值都需要更新表架构,因此要删除与解析它们相关的复杂性会以在其他地方引入另一种复杂性为代价)。
Konrad Morawski 2015年

关于元组的好点。确实,这(false, "foo")应该意味着什么?
Konrad Morawski

至少使用Guava的Optional,尝试在结果中放入null会导致异常。您必须使用Optional.absent()。
Mike Partridge

@MikePartridge好点。一个丑陋的解决方法是将地图更改为Map<String, Optional<String>>,然后最终得到Optional<Optional<String>>。较不易混淆的解决方案是使用自己的Optional,它允许allow null,或者类似的代数数据类型不允许null但具有3种状态-缺席,空值或存在。两者都很容易实现,尽管对于仅在内部使用的东西而言,涉及的样板数量很多。
2015年

2
“由于没有性能方面的考虑,并且它是一个实现细节,所以最终选择什么解决方案都没有关系。”-除非您使用的是使用C#的异常,否则任何尝试调试的人都会感到讨厌,“抛出异常时中断”为CLR例外打开了“”。
斯蒂芬

5

有一个Preferences类,它是键-值对的存储桶。空值是合法的(这很重要)。我们希望某些值可能尚未保存,并且我们希望通过在请求时使用预定义的默认值对其进行初始化来自动处理这些情况。

问题就是这样。但是您已经自己发布了解决方案:

这种方法的一种变体可以使用Guava的Optional(在整个项目中已经使用了Guava)而不是一些自定义的Tuple,然后我们可以区分Optional.absent()和“ proper” null。但是,由于易于理解的原因,它仍然让人感到有些呆滞-引入两个“无效”级别似乎滥用了最初创建Optionals背后的概念。

但是,请勿使用null Optional。您可以并且应该Optional仅使用。对于您的“骇客”感觉,只需嵌套使用它,这样一来,您Optional<Optional<String>>就可以清楚地知道数据库中可能有一个键(第一个选项层),并且它可能包含预定义的值(第二个选项层)。

这种方法比使用异常更好,并且只要Optional您不陌生,就很容易理解。

另外请注意,它Optional具有一些舒适功能,因此您不必自己完成所有工作。这些包括:

  • static static <T> Optional<T> fromNullable(T nullableReference)将您的数据库输入转换为Optional类型
  • abstract T or(T defaultValue) 获取内部选项层的键值或(如果不存在)获取默认键值

1
Optional<Optional<String>>看起来很邪恶,尽管我必须承认它有一些魅力
:)

您能进一步解释为什么它看起来邪恶吗?实际上是与相同的模式List<List<String>>。当然(如果语言允许),您可以创建一个新的类型,例如DBConfigKey,将其包裹起来Optional<Optional<String>>并使其更具可读性。
valenterry

2
@valenterry这是非常不同的。列表列表是存储某些数据的合法方法。选项(可选)是指示数据可能存在的两种问题的非典型方式。
Ypnypn

选项的选项类似于列表列表,其中每个列表都包含一个元素或不包含元素。基本上是一样的。仅仅因为Java中不经常使用它并不意味着它本质上就不好。
valenterry 2015年

1
@Jon Skeet所指的@valenterry邪恶;因此,它并不是完全坏的,而是有点邪恶的“聪明的代码”。我想这只是一个巴洛克式的,一个可选的可选。目前尚不清楚哪个级别的可选性代表什么。如果我在某人的代码中遇到它,我将采取双重行动。您可能是对的,只是因为它不寻常,单调而感到奇怪。对于函数式语言程序员(包括他们的monad以及所有对象)而言,这可能并不是什么幻想。虽然我自己也不会这么做,但我仍然为您的答案+1,因为它很有趣
Konrad Morawski

5

我知道我迟到了,但是无论如何,您的用例都类似于Java的Properties让我们也定义一组默认属性的方式,如果实例没有加载相应的密钥,则会检查这些默认属性。

看一下如何实现Properties.getProperty(String)(从Java 7开始):

Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;

正如您所引用的,确实没有必要“滥用异常来控制流程”。

@BЈовић的答案也可以使用类似但更简洁的方法:

public String getByKey(String key) {
    if (!valuesFromDatabase.containsKey(key)) {
        valuesFromDatabase.put(key, defaultValues.get(key));
    }
    return valuesFromDatabase.get(key);
}

4

尽管我认为@BЈовић的答案很好,以防万一getValueByKey,在其他地方都不需要的情况下,但如果程序包含两个用例,我认为您的解决方案不是很糟糕:

  • 如果事先不存在密钥,则通过自动创建的密钥进行检索,并且

  • 在没有这种自动性的情况下进行检索,而不更改数据库,存储库或键映射中的getValueByKey任何内容(认为​​是公开的,而不是私有的)

如果这是您的情况,并且只要性能达到可接受的水平,那么我认为您提出的解决方案是完全可以的。它具有避免重复检索代码的优点,它不依赖于第三方框架,并且非常简单(至少在我看来)。

实际上,在这种情况下,丢失的密钥是否为“例外”情况取决于上下文。对于实际情况,getValueByKey需要类似的东西。对于需要自动创建密钥的情况,提供一种方法,该方法可以重用已经可用的功能,吞噬异常并提供不同的失败行为,这是很有意义的。这可以解释为的扩展或“修饰符” getValueByKey,而不是“控制流滥用了异常”的功能。

当然,还有第三种选择:创建第三个私有方法,返回Tuple<Boolean, String>您所建议的,并在getValueByKey和中重用此方法getByKey。对于更复杂的情况,可能确实是更好的选择,但对于此处所示的简单情况,恕我直言,这有点工程过度的感觉,我怀疑代码是否真的可以这样维护。我在这里是Karl Bielefeldt的最高回答

“在简化代码时,您不应该对使用异常感到难过”。


3

Optional是正确的解决方案。但是,如果您愿意,可以选择一种哨兵,它具有较少的“两个零”的感觉。

定义一个私有静态字符串keyNotFoundSentinel,其值为new String("")。*

现在,私有方法可以return keyNotFoundSentinel而不是throw new KeyNotFoundException()。公用方法可以检查该值

String rval = getValueByKey(key);
if (rval == keyNotFoundSentinel) { // reference equality, not string.equals
    ... // generate default value
} else {
    return rval;
}

实际上,null是哨兵的特例。它是由语言定义的标记值,具有一些特殊行为(即,如果在上调用方法,则行为良好null)。就像几乎几乎所有语言都使用过这样的哨兵一样,它是如此有用。

*我们有意使用new String("")而不是仅仅""用于防止Java“中断”字符串,这将为它提供与任何其他空字符串相同的引用。因为执行了此额外步骤,我们可以确保引用的String keyNotFoundSentinel是唯一的实例,我们需要确保它永远不会出现在地图本身中。


恕我直言,这是最好的答案。
fgp

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.