【问题标题】:Pros and Cons of Inversion of Control控制反转的利弊
【发布时间】:2011-07-06 14:54:33
【问题描述】:

假设我有一个想要通过 API 公开的 [acme] 对象流。我有两个选择,回调和迭代器。

API #1:回调

// API #1
// This function takes a user-defined callback 
// and invokes it for each object in the stream.
template<typename CallbackFunctor>
void ProcessAcmeStream(CallbackFunctor &callback);

API #2:迭代器

// API #2
// Provides the iterator class AcmeStreamIterator.
AcmeStreamIterator my_stream_begin = AcmeStreamIterator::begin();
AcmeStreamIterator my_stream_end   = AcmeStreamIterator::end();

API #1 从用户手中获取程序的控制流,并且在整个流被消耗之前不会返回(暂时忘记异常)。

API #2 将控制流保留在用户手中,允许用户自行向前移动流。

API #1 感觉更更高级别,允许用户立即跳转到业务逻辑(回调函子)。另一方面,API #2 感觉更灵活,允许用户进行较低级别的控制。

从设计的角度来看,我应该选择哪一个?还有更多我还没有看到的优点和缺点吗?未来有哪些支持/维护问题?

【问题讨论】:

    标签: c++ design-patterns callback iterator inversion-of-control


    【解决方案1】:

    迭代器方法更灵活,回调版本可以通过现有算法轻松实现第一个:

    std::for_each( MyStream::begin(), MyStream::end(), callback );
    

    【讨论】:

    • 我实际上考虑过提到回调版本可以很容易地在迭代器之上提供,但忽略了它,因为(正如我们从std::string 知道的那样)一个提供多种方法的 API同样的事情太令人困惑了。我建议选择一种方式。
    【解决方案2】:

    IMO,第二个明显更胜一筹。虽然我可以(某种程度上)理解你的感觉,它的水平较低,但我认为这是不正确的。第一个定义了它自己的“更高级别”的特定概念——但它与 C++ 标准库的其余部分不太匹配,并且最终相对难以使用。特别是,它要求如果用户想要与标准算法等效的东西,则必须从头开始重新实现它,而不是使用现有代码。

    第二个与库的其余部分完美契合(假设您正确实现了迭代器)并为用户提供了通过标准算法(和/或遵循标准模式的新的非标准算法)。

    【讨论】:

    • 是的,迭代器确实更惯用。然而,它真的更好吗?我对此表示怀疑。迭代器对的 C++ 思想并不是解决迭代问题的最简单方法,我相信选择这条路线的唯一方法是指针很容易融入其中。事后看来,我认为这个决定是错误的,范围会更好。 (与最初发明 STL 的艰巨任务相比,这只是一个小批评。)
    • @sbi:例如,Windows 大量使用回调。我已经大量使用了迭代器和回调,并且看到基本上 no 有问题的余地,是的,迭代器比 很多 更好。我想要范围,但它们对迭代器的真正改进相当小——从回调到迭代器的改进要大得多
    【解决方案3】:

    与迭代器相比,回调的一个优势是您的 API 用户不会搞砸迭代。很容易比较错误的迭代器,或者使用错误的比较操作或以其他方式失败。回调 API 可以防止这种情况。

    使用回调很容易取消枚举,顺便说一句:只要让回调返回一个bool,只要它返回true就继续。

    【讨论】:

    • 我在维护过程中遇到过这种问题。我需要优先考虑处理项目的顺序。我必须找到所有使用它的代码并进行更改,而不是仅仅更改类以便每个人都免费获得更改。
    • 另外,如果你想添加多线程功能,隐藏实现比在内部提供迭代器更容易。
    • IMO,这里的推理是有缺陷的。如果您要假设 C++ 程序员不知道如何使用迭代器,您可能还需要假设他不知道如何编写函数或仿函数以用作回调。相反,如果他了解 C++,那么他确实了解迭代器。
    • 但是我喜欢通过回调方法隐藏迭代是如何完成的这一点,并且能够为未来的增强提供好处,例如 Tim 提到的多线程。
    • @Kirakun:锁定可以通过迭代器添加而无需修改调用代码,尽管正如 Tim 所说,使用 Process 函数更容易。诀窍是让迭代器持有锁。但是,默默地添加锁定通常是一个坏主意。如果调用者不知道正在使用锁,他可能会通过从回调中以“错误顺序”获取另一个锁来创建锁定反转。当然,在编写迭代器时也可以更改迭代顺序,但如果计算顺序很复杂,则可能再次比在 Process 函数中更难。
    【解决方案4】:

    C++ 标准库惯用语是提供迭代器。如果您提供迭代器,那么ProcessAcmeStreamstd::for_each 的简单包装器。也许值得写的麻烦,也许不值得,但它并没有将你的调用者提升到一个全新的可用性世界,它是一个标准库函数应用到你的迭代器对的新名称。

    在 C++0x 中,如果您还通过 std::beginstd::end 使迭代器对可用,则调用者可以使用基于范围的 for,这将它们像 ProcessAcmeStream 一样快速地带入业务逻辑,也许更快。

    所以我想说,如果有可能提供一个迭代器,那么就提供它——如果调用者想以这种方式编程,C++ 标准会为你实现控制反转。至少,对于控件如此简单的情况而言。

    【讨论】:

    【解决方案5】:

    从设计的角度来说,我会说iterator方法更好,因为它更简单也更灵活;在没有 lambda 的情况下制作回调函数真的很烦人。 (虽然现在 C++0x 将具有 lambda 表达式,这可能会变得不那么令人担忧,但即使如此,迭代器方法也更加通用。)

    回调的另一个问题是取消。你可以返回一个布尔值来表示你是否想取消枚举,但是当控制权不在我手中时我总是感到不安,因为你并不总是知道会发生什么。迭代器没有这个问题。

    当然,总是存在迭代器可以随机访问而回调不能随机访问的问题,因此它们也更具可扩展性。

    【讨论】:

      猜你喜欢
      • 2011-08-02
      • 2013-05-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-14
      • 2017-02-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多