起先,我只是想写一篇如何提升软件质量的文章。后来在写质量文章的过程中,发现我还差一部分重要的内容没讲:依赖管理。于是,我着手准备写这篇文章,结果发现我还缺少一篇关于:依赖安全机制的文章——依赖孪生。所以,也就有了上一篇文章《依赖孪生:低成本的依赖安全方案》。
笑,为了安装好软件质量这个包,我已经陷入了依赖地狱了。
回到这篇文章,我们将介绍一些依赖管理的相关实践:从基础的依赖环境隔离到告别单体应用。
事实上,针对于不同项目、不同语言进行环境隔离,本不是这篇文章讨论的范围。但是,我们又不得不提及一些相关的内容。因为,我们总是习惯性也忘记了相关的内容。
由于不同的编程语言的包管理工具,存在对于依赖的 link 机制,所以对于它们的管理也稍有不同:
virtualenv
或者是 rbenv
为每个项目创造一个独立的运行时依赖环境。node_modules
,自然而然地就进行了依赖隔离,但是需要对 Node.js 的版本进行限定。稍微一提,这种机制特别浪费硬盘。注
:由于经验有限,上述内容有些可能不太准确,如有有错误,欢迎指正。
总之,我们需要针对于不同类型的语言和包管理工具,采用与之匹配的有效的依赖环境隔离。
我们已经讨论了太多这方面的内容,这里就不详细展开了。
对于依赖的 LICENSE 进行管理和风险识别,是依赖管理的一个重要的部分。除了常见的商用管理方案之外,我们还可以采用诸如于 Node.js 项目中的 https://github.com/mwilliamson/node-license-sniffer 或者是 https://github.com/3rd-Eden/licensing 都是一些不错的工具。
至于 LICENSE 选择的问题,永远都是 MIT 优先。
这是自 left-pad 事件之后,我采取的包的新策略(在我的开源项目上):对于小的软件包,直接复制源码,在项目中创建 third-party
放置相关的代码及 LICENSE。
这主要是结合了三个要素而考虑的:
当然了内联依赖,涉及到一个版权问题——适合于代码少,不维护的项目。
除了内联之外,我们还要考虑的一个问题就是依赖的协议问题。
策略1:再发布
当我们从 Google 上搜索代码的时候,往往会遇到代码在 gist
、 stackoverflow
或者 codepen
上,它们非常适合于我们复制到项目中使用,但是难免地可能会遇到一些版权问题。对于开源项目来说,可以直接在项目中使用,并进行标即可。而对于外部项目而言,一种最简单的方式就是二次发布这些软件包。
又或者是对于一些已经不维护的依赖包,而我们又需要,那么就只能在修改后二次发布这些软件包。
策略2:二次协议 / 协议隔离层
对于 JavaScript / TypeScript 类库来说,在这方面的问题比较少,毕竟前端运行在客户端上,所以 Web 前端即『开源』。
如果真的非要使用某个开源软件,并且基于它之上修改,那么可以采取和 Android 开源协议类似的二次协议的方式。Linux 内核采用的是 GPL 协议,而 GPL 具有传染性——如果直接在 GPL 协议的类库上封装,那么只需要开源这部分的代码,因为认为我们的代码并不是衍生产品。而事实上 Free Software Foundation 动态地链接文件也产生了衍生产品,也就是说你用了 GPL 的库,你也需要开源你的代码。
为此,Android 系统在类库上采用的主要是 Apache-2.0 软件许可授权,对 GPL 进行了二次,它允许 Android 上的开发商基于 Android 的源代码进行开发而不向社区反馈。
所以,对协议进行隔离也是一个潜在可行方案,它从某种意义上将风险转移到了这一层级。
在日常的业务开发中,最痛苦的一件事莫过于:没有时间做技术升级,没有时间处理技术债。在这个时候,对于依赖更新这个问题,可能就会变得非常痛苦:你可能要亲眼目睹一个遗留系统的形成。所以,作为一个有责任的开发人员,你应该尽量去说服相关的人员,去做好相关的技术更新和升级。
除此,当依赖发现更新的时候,是否有相应的工具可以提供我们?一些现代化的工具,诸如于 Intellij IDEA 可以帮助我们更新依赖。也可以尝试包管理工具自带的升级命令,诸如于 NPM 中的: npm outdated
or npm outdated --parseable|wc -l
。不过大多数时候,我们可能会放弃升级——除非我们不得不升级。除此,https://david-dm.org 也是一个不错的工具——用于开源的公开 Node.js 项目。
相似的工具还有 next-update 是一个不错的更新工具。又或者是同一个作者写的 next-updater 的工具。
在引入依赖的时候,我们需要稍微注意一个对于依赖的版本选择问题:到是宽松型,还是严格型。依赖的版本问题,主要会出现在持续集成和新成员加入项目。诸如于使用兼容发布条款,如 ~= 0.6.4,对于那些早期已经搭建了的开发来说,并不会有问题。而新的成员安装的时候,可能就变成了 0.6.5,在 API 上可能会发现一些细微的变化 。而诸如于使用 *
匹配版本的话,就更不严谨。
以 Python 语言中的 requirements.txt
为例,存在不同的描述版本的方式:
~=: 兼容发布条款,如 ~= 0.6.4 等价于 0.6.*
==: 版本匹配条款
!=: 版本排除条款
<=, >=: 广泛的有序比较条款
<, >: 排除的有序比较条款
===: 独断的比较条款
一些常见的情况如下:
为了方便于在不同的开发人员之间能平稳地运行起项目,我们需要使用依赖锁定,以避免出现 DLL 地狱的问题。
DLL 地狱(DLL Hell)指 Microsoft Windows 系统中,因为动态链接库(DLL)的版本或兼容性的问题而造成软件无法正常执行。
为此,我们需要依赖锁定配置文件,来记录已经安装的依赖的确切版本。除此,它还描述了生成的依赖树及其关系,以便于可重现安装依赖(即在后续的安装中能够生成相同的依赖树),而忽略依赖的更新。
依赖锁定配置与依赖配置的最大区别在于,依赖配置文件只描述了需要哪些依赖及期版本,而依赖锁定配置描述了依赖的子子孙孙依赖及期关系。
如我们的一部分依赖是在内部服务器,那么在安装的时候只会从内部服务器拉取相应的依赖,不会去外部环境下载。这个时候,我们所能做的是拷贝依赖,或者是删除这个文件,重新生成。与此同时,这个依赖锁定文件可以通过源码控制工具(如 git)来进行管理。
常见的锁定配置文件有,Ruby 中的 Gemfile.lock
,Node.js 中的 package-lock.json 或者 yarn.lock。无论你如何修改 package.json
的内容,它都会按照 package-lock.json
来安装依赖。
就目前来说,常见的有两种方式:
笑~
我的意思并不是说微服务好——不过真的很香,我的主要目的是想说:大的单体应用在依赖上存在着滞后的问题。
对于微服务来说,频繁更新的服务,必然可以做出依赖的及时更新;而对于基本不修改的服务来说,我们基本不会考虑去更新依赖。对于某一长期开发的单体应用而言,它们采用新的构架和编程语言非常困难。因为某个接口的修改,导致要修改一系列的代码,有可能相当于重写整个应用。
诸如于 Spring 框架本身一直在更新,而你正在使用的版本是 2.x,那么你可能与新版本已经不兼容。甚至于,需要维护自己版本的 Spring——出于更新漏洞的原因,你们重写了系统中的部分软件。
网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。
添加我为好友,拉您入交流群!
请使用微信扫一扫!