标签(空格分隔):翻译 技术 C/C++
作者:Andrey Karpov
翻译者:顾笑群 - Rafael Gu
最后更新:2018年10月01日
假设你现在需要在你的项目中实现X功能,软件开发的专家一般都会告诉你必须要使用已经存在的Y库来实现你需要的功能。事实上,复用你自己或者其他人之前已经实现的库(第三方库)是软件研发中很经典的方式,而且绝大多数程序员都使用这种方式。
然而,这些专家门忘了在文章中或者书中提醒大家,如果在项目中对很多第三方库进行持续10年的维护,真实情况会有多糟糕。
我强烈建议避免在项目中新增不必要的库。请不要误解我,我不是让你写所有你需要的东西而不使用任何第三方库,当然,如果这样,就会让你的项目很难开展下去。但是有时候在项目中新增一些库,仅仅是因为开发者的临时起意,比如向项目中增加一些很酷的小功能。当然,向项目中增加一个新库一点也不难,但接下来整个团队都不得不在后续很多年对整个库进行维护。
在跟踪几个大项目的演化后,我发现相当多的问题都是大量第三方库依赖引起的。下面我列出其中一些问题,我也相信这个列表应该会引起一些思考和争论:
新增库会使得项目的尺寸快速膨胀。在我们这个高速互联网和大容量SSD驱动器的时代,这当然不是一个大问题。但是,当我们花费10分钟而不是1分钟从版本工具上下载这些项目时,这相当让人不快。
即使你只使用了1%的库功能,你也不得不把整个库包含到项目中。结果就是,当你通过编译好的模块(比如DLL等)使用库时,项目的尺寸就会膨胀很快;如果你通过源代码的方式使用库,编译的时间就会明显增加。
项目的链接依赖和关联结构变得更加复杂,一些库还依赖其他组件。一个简单的例子就是:我们使用Python来作编译工具,结果,有时候我们就需要很多额外的程序才能编译链接一个项目,因此编译链接失败的可能性也会增加。这很难用一言两语解释清楚,你需要有相关经验才行。在那些大的项目中,一些东西总是会失败,但你不得不花费很多努力让每样东西能够正常工作并且编译链接通过。
如果你关注库的缺陷,你就必须有规律的更新最新的第三方库(译者注:一旦某些库的缺陷被发现,这些库很快就有修复这些缺陷的新版本发布)。这(有缺陷的库)会引起别有用心的人的兴趣,他们通过学习这些库来利用这些缺陷。首先,很多库时开源的;第二,如果发现了某个库的缺陷,他们就可以攻陷使用这些库的很多项目。
库的许可证类型有可能会改变。首先你就不得不对这些库的许可证保持关注;另外,如果(许可证类型改变)真的发生了,你也不知道如何去应对。比如说,一次,一个被广泛使用的库,叫softfloat,从私人协议变为了BSD协议。
如果你升级了编译器到新版本,你就会遇到很多麻烦。一般来说,将肯定有一些库不能被新编译器编译通过,你就不得不等待,或者自己修改库代码来通过编译。
如果你使用了一个不同的编译器,你也会遇到很多问题。比如,原来你使用Vistual C++,然后想使用Intel C++,那么这肯定会有那么一两个库会发生编译错误。
如果你迁移到了不同的平台上,你还会遇到问题,这甚至会在迁移到不是完全不同的平台上发生。比如,你决定把你的应用程序从Win32迁移到Win64上,你也会遇到类似的问题。一般来说,一些库不支持这些迁移,那时你就尴尬了。特别让人不开心的就是,有些特定领域的库不再被维护和更新了。
如果你使用的时C库,很多类型不能使用namespace来限定,你就会开始发现名字冲突。这就会引起编译错误,或者潜伏的错误(译者注:运行时才会发现)。比如,你很可能会使用了错误的enum常量类型,而不是你本来想使用的那个。
如果你在项目中使用了很多库,增新一个新的库看上去也不会再增加危害了,这就好比我们使用了破窗理论(译者注:此理论认为环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉。一幢有少许破窗的建筑为例,如果那些窗不被修理好,可能将会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦没有被清洗掉,很快的,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会视若理所当然地将垃圾顺手丢弃在地上。这个现象,就是犯罪心理学中的破窗效应——来自百度百科)画了一张图,后果呢,就是随着项目的增长,我么对项目逐步变的不可控。
添加新库可能还有很多其他缺点,我可能不知道。但是无论如何,额外的库增加了项目支持的复杂性。有些问题可能会在最不希望发生的片段中出现。
我要再次强调:我并不是说我们应该完全停止使用第三方库。如果我们必须在程序中使用PNG格式的图像,我们将使用LibPNG库,而不是重复工作。
但即使是使用了PNG,我们也需要停下来思考一下。我们真的需要这个库吗?我们到底需要如何处理这些图片?如果任务只是保存图像为*.png文件,你其实只需要操作系统API就可以完成。例如,如果你有一个Windows应用程序,你可以使用WIC。如果你已经在使用MFC库,则不需要使代码更复杂,因为有一个CImage类(请参阅StackOverflow上的讨论)。少了一个库——太棒了!
让我从我自己的实践中给你们举个例子。在开发PVS-Studio分析器的过程中,我们需要在一些诊断中使用简单的正则表达式。一般来说,我相信静态分析不适合正则表达式。这是一种非常低效的方法。我甚至写了一篇关于这个话题的文章。但有时你只需要借助正则表达式在一个字符串中找到一些东西。
可以添加现有的库,但很明显,所有的库都是多余的。与此同时,我们仍然需要正则表达式,我们必须想出一些东西。
完全巧合的是,就在那个时候,我正在读一本书《美丽的代码》(ISBN 9780596510046)。这本书是关于简单和优雅的解决方案。在那里我遇到了一个非常简单的正则表达式实现。只有几十行代码,这是它!
我决定在PVS-Studio中使用这个实现。你知道吗?这个实现的能力对我们来说仍然足够;对于我们来说,复杂的正则表达式是不必要的。
结论:我们并没有添加一个新库,而是花了半个小时编写了一个需要的功能。我们抑制了再用一个库的愿望。这是一个伟大的决定;时间证明我们真的不需要那个库。我说的不是几个月,事实上,我们已经愉快地使用了(这个实现)五年多。
这个案例真的让我相信越简单的解决方案越好。通过避免添加新库(如果可能的话),您可以使项目更简单。
读者可能对上面提到的搜索正则表达式的代码感兴趣——那到底是怎样的代码。我们会从那本书上摘录,然后写在这里,看看它有多优雅。在集成到PVS-Studio时,这段代码略有更改,但其主要思想没有改变。书中的代码是:
// regular expression format
// c Matches any "c" letter
//.(dot) Matches any (singular) symbol
//^ Matches the beginning of the input string
//$ Matches the end of the input string
# Match the appearance of the preceding character zero or
// several times
int matchhere(char *regexp, char *text);
int matchstar(int c, char *regexp, char *text);
// match: search for regular expression anywhere in text
int match(char *regexp, char *text)
{
if (regexp[0] == '^')
return matchhere(regexp+1, text);
do { /* must look even if string is empty */
if (matchhere(regexp, text))
return 1;
} while (*text++ != '\0');
return 0;
}
// matchhere: search for regexp at beginning of text
int matchhere(char *regexp, char *text)
{
if (regexp[0] == '\0')
return 1;
if (regexp[1] == '*')
return matchstar(regexp[0], regexp+2, text);
if (regexp[0] == '$' && regexp[1] == '\0')
return *text == '\0';
if (*text!='\0' && (regexp[0]=='.' || regexp[0]==*text))
return matchhere(regexp+1, text+1);
return 0;
}
// matchstar: search for c*regexp at beginning of text
int matchstar(int c, char *regexp, char *text)
{
do { /* * a * matches zero or more instances */
more instances */
if (matchhere(regexp, text))
return 1;
} while (*text != '\0' && (*text++ == c || c == '.'));
return 0;
}
是的,这个版本非常简单,但是几年来这就是更复杂的解决方案所需要的。它的功能确实有限,但不需要添加任何更复杂的东西,我认为将来也不会有。这是一个很好的例子,证明了简单的解决方案比复杂的解决方案更好。
建议
不要急于在项目中添加新的库;只有在没有其他方法时才添加一个。
以下是可能的解决办法:
看看您的系统的API,或者已经使用的库中是否有必需的功能。研究这个问题是个好主意。
如果您只是使这个库的一小部分功能,那么其实你可以尝试自己实现这些功能。添加库“以防万一”的论点是站不住脚的。几乎可以肯定的是,这个库在未来不会被大量使用。程序员有时希望的是具有不切实际的通用性。
如果有几个库可以解决您的任务,请选择最简单的那个以满足您的需求。就像我之前说过的,摆脱“这是一个很酷的库——让我们用吧,以防万一”的想法。
在添加新库之前,请坐下来想一想,甚至可以休息一下,喝点咖啡,和同事讨论一下。也许您会意识到,您可以以完全不同的方式解决这个问题,而无需使用第三方库。
P.S. 我在这里所说的事情可能不是每个人都能完全接受的。例如,我建议使用WinAPI,而不是通用便携库。如果认为这样做会将项目“绑定”到一个操作系统上,那么可能会出现反对意见,然后就很难使程序拥有可移植。但我不同意这一点。通常,“然后我们将它移植到另一个操作系统”的想法只存在于程序员的脑海中。对于管理者来说,这样的任务甚至是不必要的。另一种选择是,由于项目的复杂性和通用性,在获得流行和有必要移植之前,项目将会终止。另外,不要忘记上面给出的问题列表中的第(8)点。