控制反转的优缺点

23

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

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则更加灵活,允许用户更低层次的控制。

从设计角度来看,我应该选择哪一个?还有没有我尚未发现的利弊?未来有哪些支持/维护问题?

5个回答

9

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

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

我曾经考虑过提到回调版本可以很容易地在迭代器版本之上提供,但是我放弃了这个想法,因为(正如我们从std::string中都知道的那样),提供多种完成同一件事情的方式的API只会让人感到困惑。我建议选择一种方式。 - sbi

6

我认为第二个选项明显更好。虽然我可以(有点)理解你觉得它是较低级的感觉,但我认为这是不正确的。第一个定义了自己特定的“高级”概念 - 但它并不适用于C++标准库的其余部分,并且最终变得相对难以使用。特别是,如果用户想要与标准算法等效的东西,它需要从头开始重新实现,而不是使用现有代码。

第二个选项与库的其余部分完美匹配(假设您正确实现了迭代器),并为用户提供通过标准算法(和/或遵循标准模式的新的非标准算法)以更高级的方式处理数据的机会。


是的,迭代器确实更符合惯用语。但是,它真的更好吗?我怀疑。C++中迭代器对的概念并不是处理迭代的最简单方法,我认为选择这条路的唯一原因是指针很容易适应这种方式。回想起来,我认为这个决定是错误的,范围会更好。 (与首次发明STL相比,这只是一个小小的批评。) - sbi
@sbi:例如,Windows 在很大程度上使用回调函数。我已经广泛使用了迭代器和回调函数,并且基本上没有任何疑问,即迭代器要好得多。我喜欢范围,但它们相对于迭代器的实际改进非常小——从回调到迭代器的改进要大得多。 - Jerry Coffin

5

回调函数相较于迭代器的一个优势是,API的用户无法破坏迭代过程。很容易比较错误的迭代器,或使用错误的比较操作,或以其他方式失败。回调函数API可以避免这种情况。

顺便说一下,使用回调函数可以轻松取消枚举:只需让回调函数返回bool,并只在它返回true时继续执行。


2
在维护过程中,我遇到了这种问题。我需要优先考虑处理项目的顺序。我不仅要更改类,以便每个人都免费获得更改,还必须找到使用它的所有代码并进行更改。 - Tim
此外,如果你想添加多线程能力,若先隐藏实现细节再提供迭代器访问内部数据,会更容易实现。 - Tim
3
在我看来,这里的推理是有缺陷的。如果你假设一个C++程序员不知道如何使用迭代器,那么你可能也需要假设他不知道如何编写函数或函数对象用作回调。相反地,如果他懂得C ++,那么他就知道迭代器。 - Jerry Coffin
@Tim:公平地说,迭代顺序的改变通常也可以通过更改提供迭代器的类或更改迭代器本身来实现。然而,我想有些情况下更改迭代函数(调用回调)比更改其他设置要容易得多,我无法想象还有哪种情况是相反的。 - sbi
@sbi:在我看来,std::for_each 几乎属于异常规范的同一类别。有些人认为当时这是个好主意,但他们错了。公平地说,当你使用返回的函数对象作为累加器时,它确实有一些用途。有时候,这比 std::accumulate 稍微简单一点(但在我看来仍然太少见了,不能证明其应该被包含在内)。 - Jerry Coffin
显示剩余12条评论

4
C++标准库的惯用法是提供迭代器。如果您提供了迭代器,那么ProcessAcmeStream只是std::for_each的一个简单包装。也许值得写一下,也许不值得,但它并没有将调用者推向一个全新的可用性世界,而是为您的迭代器对应的标准库函数提供了一个新名称。
在C++0x中,如果您还通过std::beginstd::end使迭代器对可用,那么调用者可以使用基于范围的for循环,这将像ProcessAcmeStream一样快速地将它们带入业务逻辑中,甚至更快。
因此,我认为,如果可能提供迭代器,则应该提供——如果调用者想以这种方式编程,C++标准将为您进行控制反转。至少,对于这种简单的控制,它确实如此。

+1。这是C++的方式,同时,通过提供迭代器API,您允许用户在您的Acme对象上使用http://www.cplusplus.com/reference/algorithm/和http://www.cplusplus.com/reference/numeric/中的所有内容。 - matiu

2
从设计角度来看,我认为迭代器方法更好,因为它更易于使用,也更加灵活;如果没有lambda函数,制作回调函数会非常麻烦。(现在C++0x将拥有lambda表达式,但即便如此,迭代器方法仍然更通用。)
回调还存在取消问题。您可以返回一个布尔值来指示是否希望取消枚举,但当控制权不在我手中时,我总是感到不安,因为您并不总是知道可能发生什么。而迭代器就不存在这个问题。
当然,还有一些问题,迭代器可以是随机访问的,而回调则不能,所以它们也更具可扩展性。

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