MethodReturnObject:Antipattern?怎么重构呢?

时间:2017-05-29 12:43:36

标签: design-patterns anti-patterns

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

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
}

此类几乎在任何地方都用作返回类型。所以方法签名看起来像这样:

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对象声称是“天才”,但对我来说它看起来像一个反模式,因为它隐藏了代码的实际功能,并为代码增加了大量开销,可以处理例外情况要好得多。 此外,我认为操纵参数是一种应该避免的副作用。

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

那么,这个“模式”是否有实际名称?它只是一种你可以忍受的代码气味,只要你保持一致吗?或者我应该尝试将该对象重构出项目以避免将来出现重大问题?可以命名哪些与此模式相关的特定问题?

PS:伪代码上面的代码,我们实际上是用VB.NET编写的。

1 个答案:

答案 0 :(得分:2)

首先,我应该说这个问题主要是基于意见的,你不应该期望获得明确的答案。话虽如此,这是我的观点。

错误代码与例外

基于错误代码和基于异常的方法都不是完美的。这就是为什么即使在今天,一些高级语言仍然没有例外(请参阅Go以获得一个新的例子)。我个人倾向于同意Raymond Chen的文章"Cleaner, more elegant, and harder to recognize",如果对所有错误进行适当的错误处理至关重要,那么编写正确的错误代码处理比正确的基于异常的代码更容易。还有一篇由ZeroMQ作者"Why should I have written ZeroMQ in C, not C++ (part I)"提出类似论点的文章。 AFAIU两篇文章的主要观点是,当您使用基于错误代码的方法时,通常会更容易看到一段代码何时无法正确处理错误,因为错误处理代码的位置更好。有了例外,您通常无法看到某些方法调用是否可以引发异常,但是未处理它(不分析整个调用层次结构的代码)。实际上,这就是为什么“检查异常”被添加到Java的原因,但由于各种设计限制,它只取得了有限的成功。

但是,错误代码不是免费的,您支付的价格的一方是您通常应该编写更多代码。在许多业务应用程序中,100%正确的错误处理并不重要,使用异常通常更容易获得“足够好”的代码。这就是为什么许多业务应用程序使用基于异常的语言和框架编写的原因。

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

特定代码

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

如果我想在.Net中使用错误代码,我的第一个想法是以类似于标准Double.TryParse的方式使用“out参数”。显然,您可以使用一些比普通Boolean更复杂的返回类型来返回有关错误的更多详细信息(例如错误代码+消息)。这种方法再次有利有弊。对我来说最大的问题是VB.NET不像C#那样明确支持out(只有ByRef这是双面的)因此它也可能会让开发人员对实际发生的事情感到困惑。

我的第二种方法是使用类似于你所展示的通用包装类来封装所有与错误相关的东西,但不包括返回值。这样的事情(我将使用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;
      }
  }
}

如果我想变得更迂腐,我可能会在SuccessValueIsSuccess时访问false并在{{1}时访问ErrorMessage时引发异常是} IsSuccess但这也可能成为许多误报的来源。

如果你想要起作用,你可能会注意到这个类实际上几乎是Monad类似于Scala Try,这是true monad在{{3}中定义的特例来自Chessie F#库的{}和Scala或类似Haskell。所以你可以使用Eithermap扩展这个类,并使用它们来做一些monad组合,但我怀疑在VB.NET语法中看起来很难看。

如果您只有几个不同的上下文,并且您希望在类型系统中编码并因此产生错误,那么从简单的flatMap切换到更通用的Try可能是有意义的处理代码更安全(有关示例,请参阅Result)。