【问题标题】:API design: is "fault tolerance" a good thing?API 设计:“容错”是好事吗?
【发布时间】:2010-07-21 13:11:29
【问题描述】:

我整合了许多有用的答案,并提出了我自己的answer below


例如,我正在编写一个 API Foo,它需要显式初始化和终止。 (应该与语言无关,但我在这里使用 C++)

class Foo
{
public:
    static void InitLibrary(int someMagicInputRequiredAtRuntime);
    static void TermLibrary(int someOtherInput);
};

显然,我们的库并不关心多线程、重入或诸如此类的问题。假设我们的 Init 函数应该只被调用一次,再次使用任何其他输入调用它都会造成严重破坏。

向来电者传达此信息的最佳方式是什么?我可以想到两种方法:

  1. InitLibrary 内部,我assert 有一些静态变量,它会责怪我的调用者进行了两次初始化。
  2. InitLibrary 中,我检查了一些静态变量,如果我的库已经初始化,我会静默中止。

方法#1 显然是显式的,而方法#2 使它对用户更友好。我认为方法 #2 的缺点可能是我的调用者不会意识到 InitLibrary 不应被调用两次这一事实。

每种方法的优缺点是什么?有没有更聪明的方法来颠覆这一切?

编辑

我知道这里的例子很做作。正如@daemon 指出的那样,我应该初始化自己而不打扰调用者。然而实际上,有些地方我需要更多信息来正确初始化自己(注意使用我的变量名someMagicInputRequiredAtRuntime)。这不仅限于初始化/终止,而是存在困境的其他情况,我应该选择引用并引用“容错”还是糟糕地失败。

【问题讨论】:

  • 这难道不完全取决于您希望对消费应用程序有多“严格”吗?
  • @Tobiasopenbrouw:也许哈哈。也许我应该标记“主观”:P
  • 如果知道应该调用多少次init(),并且可以很容易地设计库,所以知道只执行init()一次并忽略后续调用,为什么要给用户带来负担?不要强迫用户知道比他们需要的更多。

标签: idioms api-design


【解决方案1】:

我肯定会选择方法 1,以及一个易于理解的例外良好的文档解释了原因这失败了。这将迫使调用者意识到这可能会发生,并且调用类可以在需要时轻松地将调用包装在 try-catch 语句中。

另一方面,静默失败会让您的用户相信第二次调用是成功的(没有错误消息,也没有异常),因此他们会期望设置新值。所以当他们尝试用Foo 做其他事情时,他们没有得到预期的结果。如果他们无权访问您的源代码,几乎不可能弄清楚为什么。

【讨论】:

  • 执行 A 会破坏 B 时应该抛出错误。在这种情况下,调用 init() 两次不会破坏任何内容。第二次调用根本是不必要的,没有任何效果。如果有的话,应该显示一个警告。我们不应该为非致命的情况发明致命错误。
  • @Chris:如果没有例外,您如何建议显示此警告?如果不实际执行代码,编译器无法确定是否进行了多次调用。
  • 你是对的,如果需要编译时消息,那将是困难的,如果不是不可能的话。但是,作者没有指定编译时错误或警告的必要性。我暗示警告会在运行时显示在标准错误流中。
【解决方案2】:

宁静祈祷(针对界面修改)

     SA,  grant me the assertions 
     to accept the things devs cannot change 
     the code to except the things they can, 
     and the conditionals to detect the difference

如果故障出在环境中,那么您应该尝试让您的代码处理它。如果开发人员可以通过修复他们的代码来阻止它,它应该会生成一个异常。

【讨论】:

    【解决方案3】:

    一个好的方法是拥有一个创建初始化库对象的工厂(这需要您将库包装在一个类中)。对工厂的多次创建调用将创建不同的对象。这样,initialize-方法就不会成为库公共接口的一部分,工厂将管理初始化。

    如果只有一个库实例处于活动状态,请在工厂检查现有实例。这将有效地使您的库对象成为singleton

    【讨论】:

    • 单例模式不是或多或少的方法#2吗?
    【解决方案4】:

    如果您的例程无法达到预期的后置条件,我建议您标记一个异常。如果有人调用了你的 init 例程两次,并且第二次调用它后的系统状态将与刚刚调用一次一样,那么可能没有必要抛出异常。如果第二次调用后的系统状态与调用者的期望不符,则应抛出异常。

    总的来说,我认为从状态的角度思考比从行动的角度思考更有帮助。打个比方,尝试以“写入新”的方式打开已经打开的文件应该要么失败,要么导致关闭-擦除-重新打开。它不应该简单地执行空操作,因为程序将期望写入一个创建时间与当前时间匹配的空文件。另一方面,尝试关闭已关闭的文件通常不应被视为错误,因为希望关闭该文件。

    顺便说一句,提供可能引发异常的方法的“尝试”版本通常很有帮助。例如,最好有一个 Control.TryBeginInvoke 可用于更新例程之类的事情(如果线程安全控件属性发生更改,则属性处理程序希望控件在它仍然存在时进行更新,但实际上不会请注意控件是否被释放;如果控件在更新其属性时关闭,则无法避免第一次机会异常,这有点令人讨厌)。

    【讨论】:

    • 优秀的答案!我真的很喜欢你的例子。 IMO 这是一个非常好的设计理念,也很实用。
    • 我想这可以概括为“只要合约没有被破坏,一个库就应该是容错的。”
    • @kizzx2:对此,我要补充一点,应编写合约以允许库和接口在实用时以有用的方式运行。例如,我不喜欢 iEnumerable 合约的要求,即在修改基础对象时枚举器抛出异常。如果它们无法“明智地”继续(受某些要求),则要求它们抛出异常会更好,但鼓励枚举作者允许枚举继续,如果数据结构可以保持遵守合同(仅返回一次未更改的项目,等)
    【解决方案5】:

    在你的类中有一个私有的静态计数器变量。如果为 0,则执行 Init 中的逻辑并递增计数器,如果大于 0,则简单地递增计数器。在 Term 中做相反的事情,递减直到它为 0,然后执行逻辑。

    另一种方法是使用Singleton pattern,这是C++ 中的示例。

    【讨论】:

      【解决方案6】:

      我想颠覆这种困境的一种方法是满足两个阵营。 Ruby 有-w 警告开关,gcc 用户自定义-Wall 甚至-Weffc++,Perl 有污点模式。默认情况下,这些“正常工作”,但更细心的程序员可以自己开启这些严格的设置。

      反对“总是抱怨最轻微的错误”方法的一个例子是 HTML。想象一下,如果所有浏览器都对任何 CSS hack(例如在负坐标处绘制元素)大喊大叫,世界将会多么沮丧。

      在考虑了许多优秀的答案之后,我自己得出了这个结论:当有人坐下来时,我的 API 应该“正常工作”。当然,对于任何涉及任何领域的人来说,他都需要在比他试图解决的问题低一到两个抽象级别上工作,这意味着我的用户迟早必须了解我的内部结构。如果他使用我的 API 的时间足够长,他就会开始扩大限制,而过多地“隐藏”或“封装”内部工作只会变得令人讨厌。

      我想容错在大多数时候是一件好事,只是当 API 用户在扩展极端情况时很难做到正确。我可以说两全其美的是提供某种“严格模式”,这样当事情不能“正常工作”时,用户可以轻松剖析问题。

      当然,这样做需要做很多额外的工作,所以我在这里可能只是在谈论理想。实际上,这一切都取决于具体情况和程序员的决定。

      【讨论】:

        【解决方案7】:

        如果您的语言不允许此错误静态出现,则该错误很有可能仅在运行时出现。根据您的库的使用情况,这意味着该错误要等到开发的很晚才会出现。可能仅在发货时(同样,取决于很多)。

        如果默默地吃掉一个错误没有危险(无论如何这不是一个真正的错误,因为你在任何危险发生之前就发现了它),那么我会说你应该默默地吃掉它。这使它更加用户友好。

        但是,如果 someMagicInputRequiredAtRuntime 因调用而异,我会尽可能提出错误,或者库可能无法按预期运行(“我用值 42 初始化了库,但它的行为就像我以 11 开头!?”)。

        【讨论】:

        • 如果你发现程序员在代码中调用了一个他认为做某事但实际上什么也没做的错误,就会有危险。这可能会非常令人沮丧和浪费时间(“我一直在更改代码,但没有任何反应”是一个特别令人抓狂的问题)。
        • @Brian:“我一直在更改代码,但没有任何反应”——我想每个人都应该有这个问题:p
        • 我是代码问题专家,因为我遇到的问题太多了。
        【解决方案8】:

        如果这个库是一个静态类(一个没有状态的库类型),为什么不在类型初始化器中调用Init呢?如果是可实例化类型,则将调用放在构造函数中,或者放在处理实例化的工厂方法中。
        根本不允许公开访问Init 函数。

        【讨论】:

        • 如果没有状态,就不需要初始化了吧?
        • @Space_Cowboy,即使是没有状态的静态类(状态定义为特定于或定义类型实例的数据),也可能需要使用控制或配置的外部数据元素进行初始化静态方法的行为方式 - 我使用哪个数据库?我用什么格式输出结果?我今天使用什么税率表?等等等等。
        • 编辑后的Init 是一个非常人为的例子。让我们假装我们的设计实际上是需要的:p 关键是有些地方我可以选择“吃掉错误”或只是“总是抱怨”。 (或者你可以用一些巧妙的技巧来回应,在所有情况下都颠覆所有这些困境)
        【解决方案9】:

        我认为您的界面有点过于技术化。没有程序员想知道你在设计 API 时使用了什么概念。程序员想要解决他们的实际问题,不想学习如何使用 API。没有人愿意初始化你的 API,这是 API 应该尽可能在后台处理的事情。找到一个好的抽象,尽可能多地保护开发人员免受低级技术的影响。这意味着,API 应该是容错的。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-06-21
          • 1970-01-01
          • 2012-02-10
          • 1970-01-01
          • 2013-02-18
          • 2010-11-28
          相关资源
          最近更新 更多