如何避免使用异常进行流程控制?

9

我被分配了一个项目,需要开发一组类来作为存储系统的接口。其中一个要求是该类支持以下签名的get方法:

public CustomObject get(String key, Date ifModifiedSince)

基本上,该方法应该仅在对象在“ifModifiedSince”之后被修改时才返回与“key”相关联的CustomObject。如果存储系统不包含“key”,则该方法应返回null。
我的问题是:
当键存在但对象未被修改时,我该如何处理?
这很重要,因为使用此类的一些应用程序将是Web服务和Web应用程序。这些应用程序需要知道是否返回404(未找到),304(未修改)或200(好的,这是数据)。
我正在权衡的解决方案是:
1.当存储系统不包含“key”时,抛出自定义异常
2.当“ifModifiedSince”失败时,抛出自定义异常。
3.向CustomObject添加状态属性。要求调用者检查属性。
我不喜欢这三个选项中的任何一个。我不喜欢选项1和2,因为我不喜欢使用异常进行流程控制。我也不喜欢返回值,因为我的意图是指示没有值。
尽管如此,我还是倾向于选项3。
有我没有考虑的选项吗?有人对这三个选项有强烈的感觉吗?

结论:

阅读所有答案后,我得出的结论是在这种情况下使用包装器是最好的方法。本质上,我将模仿HTTP,使用包括响应代码和内容正文(消息)的元数据(标头)。

11个回答

8

看起来您需要返回两个项目:响应代码和找到的对象。 您可以考虑创建一个轻量级包装器,将它们一起保存并返回。

public class Pair<K,V>{
  public K first;
  public V second;
}

接下来,您可以创建一个新的Pair,其中包含响应代码和数据。通过使用泛型的副作用,您可以重复使用此包装器,以适用于实际需要的任何pair。

此外,如果数据尚未过期,则仍然可以返回它,但是给它一个303代码,让他们知道它没有更改。 4xx系列将与null配对使用。


1
这不错,但应该更加强类型化,而且不需要泛型化。 - Spencer Kormos
1
那个Pair类型已经非常强类型化了,为什么不想让事情尽可能地通用呢?多态是一件好事。 - Apocalisp
1
不要同时返回找到的对象和响应代码,可以返回找到的对象或失败代码之一。这可以通过使用类似于Either<A, B>的不相交联合类型来实现类型安全。 - Apocalisp
2
措辞不当。是的,它是强类型的,但“Pair”是模糊的。什么的一对?是的,你指定了“Pair<CustomObject,Integer>”,但是Integer是什么意思?有泛型,也有令人困惑的泛型。希望至少代码是在包装器中输入的。 - Spencer Kormos

5
根据所给需求,您无法完成此操作。
如果您设计了合同,则需要添加条件,并让调用者进行调用。
exists(key): bool

服务实现看起来像这样:
if (exists(key)) {
    CustomObject o = get(key, ifModifiedSince);
    if (o == null) { 
      setResponseCode(302);
    } else {
      setResponseCode(200);
      push(o);
   }

} else {
      setResponseCode(400);
}

客户端保持不变,从未注意到您已经事先验证过。 如果你没有设计合同 可能有很好的原因或者只是设计师(或架构师)的错。但既然你不能改变它,那么你也不必担心。
然后你应该遵循规格并按如下方式进行:
 CustomObject o = get(key, ifModifiedSince);

 if (o != null) {
     setResponseCode(200);
     push(o);
  } else {
     setResponseCode(404); // either not found or not modified.
  }

好的,在这种情况下,您并没有发送302,但很可能是设计的方式。

我的意思是,出于安全考虑,服务器不应返回更多信息,只需返回探针get(key,date)返回null或对象即可。

所以不要担心。与您的经理交谈,并让他知道这个决定。还要使用这个决定对代码进行注释。如果您手头有架构师,请确认这个奇怪限制背后的原理。

有可能你没有预料到这一点,他们可以在你的建议之后修改合同。

有时候,为了正确进行,我们可能会走错路,从而危及我们应用程序的安全性。

与您的团队沟通。


404表示“未找到”,304表示“未修改”。如果一个结果有两种不同的含义,那么总有一天会给某人带来麻烦。 - Dustin
1
是的,但这可能是有意为之。例如,用一个假账户登录Yahoo! 服务器会显示“无效的ID或密码”,而不是说“该账户不存在”。这样做有很好的原因。即使他们能够这样做,Yahoo也不会告诉你一个账户是否存在和密码是否错误。 - OscarRyz
1
这可能是企业安全顾问强制执行的安全限制(我们都有一个,对吧?)或者设计师只是忘记了这种情况(这从来不会发生,对吧?设计总是正确的)。我们不知道。可能这个服务曾经在内部使用,现在已经公开了。 - OscarRyz

3
您可以创建一个特殊的最终CustomObject作为“标记”,以指示未更改:
static public final CustomObject UNCHANGED=new CustomObject();

使用"=="进行匹配测试,而不是使用.equals()。

如果未改变,则可能也可以返回null,并在不存在时抛出异常? 如果我必须选择你的3个选项之一,我会选择1,因为那似乎是最特殊的情况。


1
这确实引入了一系列的if、then、else检查,每次调用该方法都必须处理。你最好使用异常,因为它们至少是强制执行的,并且更有结构性。 - Spencer Kormos
是的,但是异常引入了一系列的try/catch/finally,所以净收益只是强制执行,而代价是性能?而且异常处理更冗长,与原因代码更远。所以...这取决于情况...楼主问如何避免异常。 - Lawrence Dol

3

寻找一个不存在的对象对我来说似乎是个特殊情况。如果结合一种允许调用者确定对象是否存在的方法,那么当对象不存在时抛出异常应该是可以接受的。

public bool exists( String key ) { ... }

调用者可以执行以下操作:

if (exists(key)) {
   CustomObject modified = get(key,DateTime.Today.AddDays(-1));
   if (modified != null) { ... }
}

or

try {
    CustomObject modified = get(key,DateTime.Today.AddDays(-1));
}
catch (NotFoundException) { ... }

2
异常的问题在于它们旨在发出“快速失败”信号(即,如果未处理,则异常将停止应用程序),因为存在异常和异常行为。
我认为“键存在但对象未被修改的情况”并不是异常情况,当然也不是异常情况。
因此,我不会使用异常,而是会记录调用者需要执行的操作,以便正确解释结果(属性或特殊对象)。

是的,在这种情况下,抛出异常是一个糟糕的决定。 - OscarRyz
密钥从哪里来?如果您有密钥,则对象应该存在,如果不存在,则属于异常情况。 - tvanfosson
@Buzzer:也许是这样,但如果您的异常未经检查,应用程序必须停止吗?我不这么认为。 - VonC
然后将异常标记为已检查。未经检查的异常在当前级别上要么无法恢复,要么无法处理。它们可以向上冒泡到通知某人事件的层,然后等待采取行动。异常并不是最好的解决方案,但在这种情况下是可行的。 - Spencer Kormos

1

那个方法签名的要求有多严格?

看起来你正在进行一个仍在进行中的项目。如果你的类的使用者是其他开发人员,你能说服他们所要求的方法签名是不足的吗?也许他们还没有意识到应该有两种独特的失败模式(键不存在和对象未被修改)。

如果可以的话,我建议你与你的主管讨论一下。


1

我仍然会返回null。

属性的意图是在指定日期之后返回被修改的对象。如果对于没有对象返回null是可以接受的,那么对于未修改的对象返回null也可以接受。

个人而言,我会为非修改的对象返回null,并为不存在的对象抛出异常。这似乎更自然。

顺便说一句,你不使用异常进行流程控制是完全正确的,所以如果你只有这三个选项,你的直觉是正确的。


这是唯一合理的解决方案。如果方法的限定符不满足,则应返回null,我看不出还有其他意义。无效键上的null值是有问题的,可以采取任何一种方式。Map将返回null,其他类将抛出IllegalArgumentException。 - Robin
当然,无效的键情况已经确定,因此两者都为null。 - Robin

1
你可以遵循 .Net 库的模式,在自定义对象中添加一个名为 CustomObject.Empty 的公共静态只读字段,其类型为 CustomObject(类似于 string.Empty 和 Guid.Empty)。如果对象未被修改,您可以返回此对象(函数使用者需要与之进行比较)。 编辑:我刚刚注意到您正在使用 Java,但原则仍然适用。 这使您有以下选项:
  • 如果键不存在,则返回 null。

  • 如果键存在但对象未被修改,则返回 CustomObject.Empty

缺点是使用者需要知道 null 返回值和 CustomObject.Empty 返回值之间的区别。
也许将属性更恰当地称为 CustomObject.NotModified 更合适,因为空值实际上是针对值类型而言的,它们不能为 null。此外,NotModified 更容易向使用者传达字段的含义。

1

关于需求的(预期)接口严重损坏。您试图在一个方法中做不相关的事情。这是通往软件地狱的道路。


阿门!从那里没有回头的路。 - OscarRyz

1

提供一个回调函数作为参数,其中回调类可以是事件驱动或设置器驱动。

您可以定义类的接口来定义可能发生的各种错误,并在需要时将CustomObject作为事件的参数传递。

public interface Callback {
  public void keyDoesNotExist();
  public void notModified(CustomObject c);
  public void isNewlyModified(CustomObject c);
  .
  .
  .
}

通过这种方式,您允许Callback接口的实现者定义事件发生时要执行的操作,并且可以通过该接口选择是否需要传递检索到的对象来满足某些条件。最后,它减少了返回逻辑的复杂性。您的方法仅需执行一次此操作。API的实现者根本不需要执行此操作,因为已经为他们执行了。

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