哪种结果模式对于公共API来说是最好的?为什么?

6
有几种常见的模式可以用于在公共API中返回函数调用的结果。哪一种是最佳实践并不明显。是否有普遍共识或者说令人信服的理由,说明为什么其中一种模式比其他模式更好呢?
更新:通过公共API,我指的是暴露给依赖程序集的公共成员。我仅仅是指作为Web服务公开的API。我们可以假设客户端正在使用.NET。
下面我写了一个示例类来说明不同的返回值模式,并且我已经注释了每个模式表达我的担忧。
这是一个有点长的问题,但我相信我不是唯一考虑过这个问题的人,希望这个问题对其他人也有趣。
public class PublicApi<T>       //  I am using the class constraint on T, because 
    where T: class              //  I already understand that using out parameters
{                               //  on ValueTypes is discouraged (http://msdn.microsoft.com/en-us/library/ms182131.aspx)

    private readonly Func<object, bool> _validate;
    private readonly Func<object, T> _getMethod;

    public PublicApi(Func<object,bool> validate, Func<object,T> getMethod)
    {
        if(validate== null)
        {
            throw new ArgumentNullException("validate");
        }
        if(getMethod== null)
        {
            throw new ArgumentNullException("getMethod");
        }
        _validate = validate;
        _getMethod = getMethod;
    }

    //  This is the most intuitive signature, but it is unclear
    //  if the function worked as intended, so the caller has to
    //  validate that the function worked, which can complicates 
    //  the client's code, and possibly cause code repetition if 
    //  the validation occurs from within the API's method call.  
    //  It also may be unclear to the client whether or not this 
    //  method will cause exceptions.
    public T Get(object argument)
    {
        if(_validate(argument))
        {
            return _getMethod(argument);
        }
        throw new InvalidOperationException("Invalid argument.");
    }

    //  This fixes some of the problems in the previous method, but 
    //  introduces an out parameter, which can be controversial.
    //  It also seems to imply that the method will not every throw 
    //  an exception, and I'm not certain in what conditions that 
    //  implication is a good idea.
    public bool TryGet(object argument, out T entity)
    {
        if(_validate(argument))
        {
            entity = _getMethod(argument);
            return true;
        }
        entity = null;
        return false;
    }

    //  This is like the last one, but introduces a second out parameter to make
    //  any potential exceptions explicit.  
    public bool TryGet(object argument, out T entity, out Exception exception)
    {
        try
        {
            if (_validate(argument))
            {
                entity = _getMethod(argument);
                exception = null;
                return true;
            }
            entity = null;
            exception = null;   // It doesn't seem appropriate to throw an exception here
            return false;
        }
        catch(Exception ex)
        {
            entity = null;
            exception = ex;
            return false;
        }
    }

    //  The idea here is the same as the "bool TryGet(object argument, out T entity)" 
    //  method, but because of the Tuple class does not rely on an out parameter.
    public Tuple<T,bool> GetTuple(object argument)
    {
        //equivalent to:
        T entity;
        bool success = this.TryGet(argument, out entity);
        return Tuple.Create(entity, success);
    }

    //  The same as the last but with an explicit exception 
    public Tuple<T,bool,Exception> GetTupleWithException(object argument)
    {
        //equivalent to:
        T entity;
        Exception exception;
        bool success = this.TryGet(argument, out entity, out exception);
        return Tuple.Create(entity, success, exception);
    }

    //  A pattern I end up using is to have a generic result class
    //  My concern is that this may be "over-engineering" a simple
    //  method call.  I put the interface and sample implementation below  
    public IResult<T> GetResult(object argument)
    {
        //equivalent to:
        var tuple = this.GetTupleWithException(argument);
        return new ApiResult<T>(tuple.Item1, tuple.Item2, tuple.Item3);
    }
}

//  the result interface
public interface IResult<T>
{

    bool Success { get; }

    T ReturnValue { get; }

    Exception Exception { get; }

}

//  a sample result implementation
public class ApiResult<T> : IResult<T>
{
    private readonly bool _success;
    private readonly T _returnValue;
    private readonly Exception _exception;

    public ApiResult(T returnValue, bool success, Exception exception)
    {
        _returnValue = returnValue;
        _success = success;
        _exception = exception;
    }

    public bool Success
    {
        get { return _success; }
    }

    public T ReturnValue
    {
        get { return _returnValue; }
    }

    public Exception Exception
    {
        get { return _exception; }
    }
}

是否有任何完整源代码的最终解决方案? - Kiquenet
@Kiquenet,我喜欢使用类似于代码示例中的Result<T>类,但具有自定义的相等运算符/重载和双向隐式转换运算符以便于使用。 - smartcaveman
3个回答

6
  • Get - 如果验证失败是意外的或者调用者在调用方法之前可以自行验证参数,则使用此选项。

  • TryGet - 如果预期会出现验证失败,请使用此选项。由于.NET Framework中常见的使用(例如Int32.TryParseDictonary<TKey,TValue>.TryGetValue),因此可以假定TryXXX模式已经熟悉了。

  • TryGet with out Exception - 异常可能表示传递给类的代码委托中存在错误,因为如果参数无效,则_validate将返回false而不是抛出异常,并且不会调用_getMethod

  • GetTupleGetTupleWithException - 以前从未见过这些。我不建议使用它们,因为元组不是自解释的,因此不是公共接口的好选择。

  • GetResult - 如果_validate需要返回比简单布尔值更多的信息,则使用此选项。我不会将其用于包装异常(请参阅:TryGet with out Exception)。


3
我会尽力进行翻译,以下是需要翻译的内容:我还要补充一点,记录抛出异常的标准方法是使用内联XML文档。这些异常会显示在Intellisense中,并且在使用像Sandcastle这样的工具导出帮助文件文档时也会被使用。因此,我强烈建议 不要 使用 out Exception 模型(如果你想重新抛出,这通常也会破坏堆栈跟踪)。 - Dan Bryant
关于异常。如果情况是这个库正在调用外部资源,而该调用有时可能会失败(网络故障),但您不希望它杀死整个应用程序。那么拥有一个类来返回操作未成功并具有异常的附加信息是否有效? - Dzyann
关于 out Exception 破坏堆栈跟踪的问题:如果调用者准备解析嵌套异常,那么这可能是可以接受的。然而,典型的 C# 代码通常不会解析嵌套异常。(NUnit 是解析嵌套异常的一个例子。) - rwong

1
如果你所说的“公共API”是指将被应用程序在你的控制之外消耗,并且这些客户端应用程序将使用各种语言/平台,我建议返回非常基本的类型(例如字符串、整数、小数),并使用类似JSON的东西来处理更复杂的类型。
我认为你不能在公共API中公开一个通用类,因为你不知道客户端是否支持泛型。
从模式上讲,我倾向于采用REST-like方法而不是SOAP。Martin Fowler在他的一篇高层次文章中对此有很好的解释:http://martinfowler.com/articles/richardsonMaturityModel.html

我明白了。那么我的回答完全不合适。 - Hector Correa

-2

考虑事项:

1- 关于DOTNet编程语言和Java,有一个特殊情况,由于您可以轻松检索对象,而不仅限于原始类型。例如:因此,“普通C” A.P.I.可能与C# A.P.I.不同。

2- 如果在检索数据时您的A.P.I.存在错误,如何处理而不中断应用程序。

答案:

我看过几个库中的一种模式是一个函数,其主要结果是一个整数,其中0表示“成功”,另一个整数值表示特定的错误代码。

该函数可能具有多个参数,大多数为只读或输入参数,以及单个referenceout参数,该参数可以是原始类型的引用,也可以是指向可能更改的对象的引用,或者是指向对象或数据结构的指针。

在异常情况下,一些开发人员可能会捕获它们并生成特定的错误代码。

public static class MyClass
{

  // not recomended:
  int static GetParams(ref thisObject, object Param1, object Params, object Param99)
  {
    const int ERROR_NONE = 0;

    try
    {
      ...
    }
    catch (System.DivideByZeroException dbz)
    {
      ERROR_NONE = ...;
      return ERROR_NONE;
    }
    catch (AnotherException dbz)
    {
      ERROR_NONE = ...;
      return ERROR_NONE;
    }

    return ERROR_NONE;
  } // method

  // recomended:
  int static Get(ref thisObject, object ParamsGroup)
  {
    const int ERROR_NONE = 0;


    try
    {
      ...
    }
    catch (System.DivideByZeroException dbz)
    {
      ERROR_NONE = ...;
      return ERROR_NONE;
    }
    catch (AnotherException dbz)
    {
      ErrorCode = ...;
      return ERROR_NONE;
    }

    return ERROR_NONE;
  } // method

} // class

这类似于你的元组结果。干杯。

更新1:提到异常处理。

更新2:显式声明常量。


1
通常不建议在支持异常处理的编程语言中使用错误代码。 - dtb
1
错误代码最大的挑战在于它们创建了一个“假设最好”的流程。如果开发人员忘记检查代码(这种情况经常发生),执行将继续进行,即使之前的步骤已经失败。这通常会导致一系列的故障,直到最终出现严重的软件崩溃。异常是一种“假设最坏”的模型,您必须明确说明特定的故障情况是可以接受的。这会导致没有考虑错误的代码尽早终止。 - Dan Bryant
我对这种方法的疑问是,它是否被认为是面向对象编程(OOP)?在我看来似乎不是。它并不是真正的自我说明性。在某些情况下,您可能会将一组异常捕获到一个错误代码中,因此使用库的人可能会错过非常重要的信息。我认为创建一个实际的对象来返回条件更好。您甚至可以为不同的错误创建类层次结构,这样使用者就可以使用多态来处理异常。 - Dzyann
@Dan Bryant。你的回答是“建设性批评”的好例子。在面向对象编程中,我同时使用异常和返回码,但这也取决于应用程序的逻辑或流程。 - umlcat
@Dziann,我同意错误代码更适合过程式编程而不是面向对象编程。但是,在使用多个API,特别是共享库时,有些情况下错误代码似乎是更好的工具。我大部分时间都在使用面向对象编程,但是在某些情况下,使用函数式、过程式或其他范式更加合适。干杯。 - umlcat
显示剩余3条评论

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