MethodReturnObject:反模式?如何重构它?

MethodReturnObject: Antipattern? How to refactor it?

在我最近加入的一个项目中,我们有一个 class 看起来像这样:

class MethodReturn {
  public int    status    = C_OK
  public string message   = ""
  public string str_value = ""
  public int    int_value = 0
  public double dbl_value = 0
  // and some more

  // also some methods that set/get these values
}

这个 class 几乎在任何地方都被用作 return 类型。所以方法签名看起来像这样:

public MethodReturn doSomething( int input, int output )

调用代码如下所示:

returnObj = doSomething( input, output )
if( returnObj.status != C_OK ) return returnObj

returnObj2 = doSomethingElse( input2, output2)
if( returnObj2.status != C_OK ) return returnObj2

MethodReturn 对象声称是 "genius",但对我来说它看起来像一个反模式,因为它隐藏了代码的实际功能并给代码增加了很多开销,异常可以更好地处理。 此外,我认为对参数的操纵是应该避免的副作用。

不幸的是,这些 MethodReturn 对象被提供给 GUI 以显示操作结果或在发生错误时显示消息。

那么,这个 "pattern" 有一个真实的名字吗?只要您保持一致,它是否只是您可以忍受的代码味道?还是我应该尝试将该对象重构出项目以避免将来出现严重问题?与此模式相关的具体问题有哪些?

PS:上面的伪代码,我们实际编码在VB.NET.

首先我要说的是,这个问题很大程度上是基于个人意见的,您不应期望得到明确的答案。话虽如此,这是我的看法。

错误代码与异常

无论是基于错误代码还是基于异常的方法都不是完美的。这就是为什么即使在今天,一些高级语言仍然没有例外的原因(参见 Go 的新示例)。我个人倾向于同意 Raymond Chen 的文章"Cleaner, more elegant, and harder to recognize" that if proper error handling of all errors is of utmost importance, it is easier to write correct error-code handling than correct exception-based code. There is also an article with similar argument by ZeroMQ author "Why should I have written ZeroMQ in C, not C++ (part I)"。 AFAIU 这两篇文章的要点是,当您使用基于错误代码的方法时,通常更容易看到一段代码何时没有正确处理错误,因为错误处理代码的局部性更好。对于异常,您通常看不到某些方法调用是否可以引发异常但未处理(不分析整个调用层次结构的代码)。实际上这就是 "checked exceptions" 被添加到 Java 的原因,但由于各种设计限制,它只取得了有限的成功。

但是错误代码不是免费的,您付出的代价之一是您通常应该编写更多代码。在许多业务应用程序中,100% 正确的错误处理并不那么重要,使用异常通常更容易获得 "good enough" 代码。这就是为什么许多业务应用程序是使用基于异常的语言和框架编写的。

通常对我来说,这类似于垃圾收集器与手动内存管理的决定。在某些情况下,您确实应该使用错误代码或手动内存管理,但对于大多数用例,异常和 GC 就足够了。

具体代码

尽管我认为错误代码与异常是一个悬而未决的问题,并且应该根据特定的项目上下文和目标做出设计决策,但我真的不喜欢您在问题中显示的代码。我真正不喜欢的是单个 MethodReturn 包含所有可能的 return 类型。这是一件坏事,因为它向开发人员隐藏了真正的方法 return 类型。

如果我想在 .Net 中使用错误代码,我的第一个想法是以类似于标准 Double.TryParse 的方式使用 "out parameters"。显然,您可以使用一些更复杂的 return 类型,而不是简单的 Boolean 到 return 更多关于错误的详细信息(例如错误代码 + 消息)。同样,这种方法有利有弊。对我来说最大的缺点是 VB.NET 不像 C# 不明确支持 out (只有 ByRef 是双向的)所以它也可能让开发人员混淆真正发生的事情。

我的第二种方法是使用类似于您展示的通用包装器 class 来封装所有与错误相关的东西,但不封装 return 值。类似这样的东西(我将使用 C#,因为我更熟悉它):

class MethodReturn<TValue> {
  private readonly int    _status;  
  private readonly string _errorMessage;
  private readonly TValue _successValue;

  private MethodReturn(int status, string errorMessage, TValue successValue) {
        _status = status;
        _errorMessage = errorMessage;
        _successValue = successValue;
  }

  public static MethodReturn<TValue> Success(TValue successValue)  {
      return new MethodReturn(C_OK, null, successValue);
  }

  public static MethodReturn<TValue> Failure(int status, string errorMessage)  {
      return new MethodReturn(status, errorMessage, default(TValue));
  }

  public int Status { get { return _status; } }
  public string ErrorMessage { get { return _errorMessage; } }
  public int SuccessValue { get { return _successValue; } }

  public bool IsSuccess {
      get {
          return _status == C_OK;
      }
  }
}

如果我想更迂腐一点,如果在 IsSuccessfalse 时访问 SuccessValue 并且在 ErrorMessage 时访问 ErrorMessage ,我也可能会引发异常 IsSuccesstrue 但这也可能成为大量误报的来源。

如果您想发挥功能,您可能会注意到这个 class 实际上几乎是来自 Chessie F# 库的 Monad similar to Scala Try which is a special case of Either monad defined in Scala and Haskell or similar Result。因此,您可以使用 mapflatMap 扩展此 class 并使用它们进行一些单子组合,但我怀疑在 VB.NET 语法中看起来很难看。

如果您只有几个不同的上下文,其中包含您想要在类型系统中编码的不同错误代码列表,那么从更简单的 Try 切换到更通用的 Either 可能是有意义的,因此使错误处理代码更安全(参见 this F#/Result article 示例)。