你可能不需要引入一个库

使用人力仿佛神力不在,利用神力宛如人力乌有
—— 葛拉·西安

库的介绍

编程语言中的「库」或者说「包」,是完成一系列特定任务的代码集合,是由一些程序员使用特定语言抽象出来的工具集。库分为两种: 一种是编程语言提供的,由编程语言的维护者编写维护,我们一般称之为标准库,这些标准库也是编程语言的基石; 另一种是由其他的程序员开发并开放出来给人们使用的,这些包我们一般叫做第三方库。一种语言的受欢迎程度通常也与库的丰富度有关,不仅仅是标准库,还有第三方库。第三方库越活跃,反过来也说明了这门语言很受人欢迎。

库在我们的日常开发中起着不可或缺的作用,如果没有了这些库,一切的开发任务都要从零开始。对我们而言这绝对是一场噩梦,我们可能需要研究各种各样的通讯协议,可能会深陷在各种参数判断,各种数据处理,各种返回体的构造和解析中。所以我们往往会选择那些已经提供很多库的编程语言或框架,因为它们减少了我们很多工作量,避免了很多重复劳动。

因为标准库由编程语言的维护人员进行编写维护,一般是一些常用的类库,如一些标准输入输出的处理,文件处理,日志服务以及网络请求等等,这里我们不做过多赘述,在这篇文章中我们主要讨论第三方库。

第三方库的功能

通常第三方库的功能是对一些业务的封装,可能是一些数据的处理,或者是第三方服务的对接。对于使用者而言,只需要在包管理工具的配置文件中引入,并执行安装命令,然后就可以在项目中使用了。下面以对接一个 OpenAI 的 API 为例子,对接的接口是对话接口,使用 Ruby 语言, Ruby 中的库官方名称为 Gem,这里使用的 Gem 是 Ruby-OpenAI

Ruby 中的包管理工具为 Bundler ,对应的配置文件名为 Gemfile ,所以安装这个 Gem 的第一步是在 Gemfile 里边加入下面这一行:

gem "ruby-openai"

然后执行 bundle install 即可。

使用这个库的原因是:它为我们封装好了对应的调用 OpenAI 接口的逻辑,我们只需要用以下代码就可以对接上 OpenAI 对话接口了

token = "xxx"
client = OpenAI::Client.new(access_token: token)
client.chat(
  parameters: {
    model: "gpt-3.5-turbo",
    messages: [{ role: "user", content: "Hello" }],
    stream: proc do |chunk, _size|
      puts chunk
    end
  }
)

上面这段代码的作用是:使用流式接收响应的形式调用对话接口,在接收到响应片段的时候,将响应里边的内容打印出来。

这里我们用了短短几行就对接上了对话接口,不需要了解接口的地址,接口具体使用的是什么协议,使用什么方式,token 以什么形式传入,花在探索和调试这些问题的时间也省下来了,这让我们可以专注的投入的我们想要实现的核心功能上。这便是这些第三方库给我们带来的最大好处。

不过,这个世界上没有免费的午餐,所有的事情都有两面性。第三方库给我们带来便利的同时,也带来了一些问题。

第三方库的问题

冗余功能

还是用上面对接 OpenAI 的例子,一个对接第三方的服务通常不会只提供一个接口的封装,往往还会封装一些其他的功能在里边。比如这里用到的 Ruby-OpenAI 的库,它里边还封装了:文本补全、图片生成、图片编辑、语音转文字等功能。而这些功能是我们目前所不需要的,在我们引入该库的同时,这些功能的代码也进入到了我们的项目里。这些功能是库的作者为了功能完善而添加上去的,还有一部分可能是作者的偏好或者是其他使用者的需求,所以我们也无法令作者去删减这些功能。

依赖

库的开发同我们项目的开发并没有什么不同,因此,一些库在开发的过程中,出于跟我们使用库相同的目的,也会引入一些其他的库,另外的这些库成为了正在开发的库的依赖。当我们引入这个库的时候,这个库成为了我们的依赖,而这个库的依赖也会成为我们的依赖。如果库在少的情况下可能不会有什么问题,但是随着我们项目的一步步壮大,在我们不断地往项目里边引入库的过程中,库与库之间的依赖会形成一张网,当我们想要引入一个新的库,并且该库所依赖的其他库的版本比我们项目里边其他库的所依赖的要新的时候,这个库就没有办法引进来了。

除了库对其他库的依赖对我们的项目有影响外,库所依赖的编程语言或框架的版本也会对我们造成制约。比如说,一个 Ruby 的库依赖 Ruby 的版本为 3.0 ,我们的项目一开始使用的版本也是 3.0 ,但是随着 Ruby 版本的不断更新,我们决定升级到 3.2 了,但是我们发现这个库的更新没有那么频繁了,还是保留在 3.0 的阶段。类似的情况在少的情况下,我们也许能通过提 PR 或者 Fork 的方式处理,如果多的话,我们也束手无策。

上述的原因可以归结为:第三方库独立于我们的项目,是不受我们掌控的——这同样是我们所期望的,这是造成上面这些问题的根本原因

维护

库的维护者们通常没有从项目的维护过程中直接受利,而且大多数情况下需要牺牲自己的私人时间去维护——这也是开源工作者值得尊敬的地方。所以在经过一段时间的维护后,有些成员因为家庭的原因可能没有时间去维护了,而一部成员也没有动力继续去维护了,再加上没有新鲜血液加入,这样就会导致项目搁置。项目中的依赖没有人去升级,这会加重上述的依赖问题,项目中的 issue 也没有人去解决,这可能会给使用者们带来安全隐患

种类太多

没错,这也是个问题。因为每个人的偏好不同,市面上也会出现不同风格的包,这些包实现了相同或类似的功能,这种情况也会加重上面依赖的问题,很可能我们引入的不同的库依赖不同的 HTTP 请求库,毕竟我见到过一个 Rails 项目里边同时出现了: Faraday, RestClient, httprb 这几个包...

思考

正是因为有上面出现的这些问题,所以我们在引入一个新的库的时候才需要进行慎重考量。我们需要根据实际情况进行分析,来决定我们是否有必要引入这样的一个库,我们能否使用标准库来封装我们所需要的接口?如果可以,我们可能并不需要引入相关的库。也许我们会因此多出一些工作量,但是我们得到的好处是没有依赖的自由,我们可以随时升级编程语言和框架的版本,而不需要顾虑对第三方库的影响。而如果我们只是需要实现一个原型,做一个 idea 的检验,或者我们不确定该项目是否会长期维护下去的时候,使用第三方库来减少我们的工作量也是一个不错的选择。

还是 OpenAI 的例子,这次使用标准库

出于减少依赖的考量,因为我在自己的项目中只用到了对话的接口,而 Ruby-OpenAi 这个库还依赖 Faraday 和 Faraday-Multipart 这两个库,所以我决定移除它,自己使用 Net::HTTP 这个标准库来对接接口。下边是对接的代码

uri = URI("https://api.openai.com/v1/chat/completions")
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  token = "xxx"
  request = Net::HTTP::Post.new(uri)
  request["Content-Type"] = "application/json"
  request["Authorization"] = "Bearer #{token}"
  request.body = {
    model: "gpt-3.5-turbo",
    messages: [{role: 'user', content: "hello"}],
    stream: true,
  }.to_json

  http.request(request) do |response|
    response.read_body do |chunk|
      puts chunk
    end
  end
end

这里用了不到 20 行的代码,就实现了同样的功能,还少了三个依赖,这三个依赖的代码加起来也有数千行的代码了。当然,这个例子比较极端,很可能我们的项目里边已经存在 Faraday 这样的依赖了, 也许再引入一个 Ruby-OpenAI 也不是什么大的问题。但是从软件工程的角度来说,依赖能少就少。

评论