通常的做法是从实现中抽象库依赖项吗?

时间:2012-09-11 15:07:52

标签: c++ architecture frameworks software-design

我对这个问题的回答是“不”。但我的同事不同意。

我们正在重建我们的产品,并在短期内做出许多关键决策。

在做我自己的一些工作时,我注意到我们有一些内部C ++类来抽象一些POSIX API(线程,互斥,信号量和rw锁)和其他实用程序类。请注意,这些类是基本的,并且尚未从Linux移植(可移植性是重建的一个因素。)我们也使用POCO C ++库。

我提请同事注意这一点,并建议我们放弃我们的内部课程以支持他们的POCO等同物。我想充分利用我们已经使用的库。他们建议我们应该使用POCO实现我们的内部类,并在必要时进一步抽象出额外的POCO类,以便不依赖于任何特定的C ++库(引用未来的未知数 - 如果我们想要使用不同的lib /框架,那该怎么办? QT或提升,如果我们选择的那个结果证明不好或者开发变得不活跃等等。)

他们也不想重构遗留代码,并且通过使用我们自己的类抽象POCO的部分,我们可以实现其他功能(经典OOP。)我可以理解这两个参数。但是,我认为,如果我们正在进行重新编码,我们应该变大,或者回家。现在是重构的时候了,它确实应该不是那么糟糕,特别是考虑到我们的类和POCO(线程等)之间的相似性我不知道对于第二点说什么 - 我们应该只使用需要功能的扩展类?

我的同事也不想在整个地方乱丢POCO名称空间。我认为我们应该选择一个库/框架/工具包,并坚持下去。充分利用其功能。这不是典型的做法吗?我见过的唯一一个抽象整个框架的项目是Freeswitch(它为APR提供了自己的接口。)

一个建议是,我们向对方和潜在客户公开的API应该没有POCO,但它会出现在实现中(这是有意义的。)

我们当中没有人真正拥有这些设计决策的经验,并且在当前产品中显示出来。从我年轻的时候就一直在这里,我有一些直觉让我来到这里,但也没有实际经验。我真的想避免解决已经解决的问题。

我认为我的问题归结为:在构建产品时,我们是否应该a)选择一个支配大部分代码的主导框架,以及b)期望该框架与产品紧密结合?这不是框架的重点吗? (框架或库是否更适合POCO?)

2 个答案:

答案 0 :(得分:4)

首先,您公开的API绝对应该没有POCO,boost,qt或任何其他不属于标准C ++库的类型。这是因为基本库有自己的发布周期,与库的发布周期不同。如果您的库的用户也使用boost,但是使用不同的,不兼容的版本,则他们需要花时间来解决不兼容问题。此规则的唯一例外是当您设计要作为更广泛框架的一部分发布的库时 - 例如,POCO工具包的补充。在这种情况下,库的发布与整个工具包的发布相关联。

但是,在内部,您应该避免使用自己的包装器,除非您抽象出来的库是真正的“商品库” 1 。这样做的原因是当你在类后面隐藏一个外部库时,大多数时候你模仿你隐藏的库的抽象级别。使用您的包装器的代码将编程到外部库指定的抽象级别。当您将包装器后面的实现交换为不同的框架时,您很可能(1)调整新框架以适应旧框架的抽象级别,或者(2)需要更改方式你使用你的包装器。这两种情况都非常可疑:如果你这样做(1),也许你不应该在第一时间切换,如果你这样做(2),那么你的包装被证明是没用的。

<小时/> 1 “商品库”是指一个库,它提供了其他库中常见的抽象级别,这些抽象级别用于类似的目的。

答案 1 :(得分:4)

有两种情况我认为值得拥有自己的包装器:

1)您已经在不同的系统/库上查看了几种不同的互斥体实现,您已经建立了一组通用的需求,它们都可以满足您的软件需求。然后,您可以定义该抽象并将其实现一次或多次,因为您知道自己已经提前计划了灵活性。编写的其余代码是为了依赖抽象,而不是依赖于当前实现的任何附带属性。我过去做过这个,虽然不是代码我可以告诉你。

这个“最不常见的接口”的一个典型示例是在文件系统抽象中更改rename,因为Windows无法实现原子重命名现有文件。所以你的代码必须不依赖于原子重命名替换,如果你将来可能会替换当前的* nix实现而不能那样做。您必须从一开始就限制接口。

如果做得好,这种界面可以显着简化任何类型的未来移植,无论是新系统还是因为您想要更改第三方库依赖关系。但是,整个框架可能太大而无法成功实现这一点 - 基本上你是在发明和编写自己的框架,这不是一项简单的任务,可以想象的是比编写实际软件更大的任务。

2)你希望能够模拟/ stub / sham / spoof / plagiarize /无论下一个聪明的技术是什么,测试中的互斥体,并决定如果你有自己的包装器,你会发现这更容易而不是你试图弄乱第三方库中的符号,或者内置的符号。

请注意,定义您自己的函数wrap_pthread_mutex_initwrap_pthread_mutex_lock等,精确模仿pthread_*函数,并采用完全相同的参数,可能会满足(2)但不能't 满足(1)。无论如何,正确地执行(2)可能需要的不仅仅是 包装器,您通常也希望将依赖项注入到代码中。

在灵活性的标题下做额外的工作,而不是实际提供灵活性,这几乎是浪费时间。根据另一个线程环境实现一个线程环境可能非常困难甚至可能不可能。如果您决定将来在p ++中从pthread切换到std::thread,那么使用一个看起来与pthreads API完全相同的抽象的(大致)没有任何帮助。

对于您可能进行的另一项可能的更改,在Windows上实现完整的pthreads API有点可能,但可能比仅实现您实际需要的更难。因此,如果您移植到Windows,所有抽象可以节省您的时间来搜索和替换其余软件中的所有调用。您仍然需要(a)为Windows插入完整的Posix实现,或者(b)完成工作以找出您实际需要的内容,并且只实现它。你的包装器对实际工作没有帮助。