构建一个 Ruby Gem 第九章 加载

回到那个使用中划线和下划线没有很多规范的时代, 很多库选择了前者。这导致在当下划线成为标准时,用户感到非常困惑。

还记得当我们的讨论关于在 mega_lotto.gemspec 文件的头部声明加载路径吗? 嗯…当我们在一个 Gemfile 中指定一个 gem 时 bundler 加载了同和我们的加载路径同名的文件。 并且因为我们加入了我们的 gem 的 lib/ 目录到加载路径(bundler 已经在 gemspec 文件中帮我们设定好了),我们的入口文件已经被加载了。

所以通过加入 mega_lotto 到 Gemfile 中, bundler 知道去寻找入口文件 lib/mega_lotto.rb. 这都是约定俗成的,因为你知道,我们 Rubyist,热爱我们的约定。

不是所有的类库都需要在运行时被加载。在入口文件声明依赖取决于我们。这是非常常见的一个 gem 中的类库不是被默认加载的。 一些例子是 命令行 可执行文件的类,web 引擎和支持测试的类库。

在本章中,我们将会讨论关于加载一个可选类库就像上面提到的最佳实践和看看一些在我们的 gem 中扩展 Ruby 基本类库的方法。

支持类库

假设我们为我们的 mega_lotto gem创建一个 web 引擎并且想要在一个宿主应用中被加载。 支持 web 界面的类库可能位于 lib/mega_lotto/web.rb 如果是这样我们可以在 Gemfile 中 require 它这样它就会被自动加载了:

或者,也许,在一个 Rails 的 initializer 中和一些其它的配置文件一起加载它:

# config/initializers/mega_lotto.rb

require "mega_lotto/web"
# other gem configuration...

在一个 Rails 应用中, 我们可以像 config/routes 文件那样挂载一个类库:

mount MegaLotto::Web => '/mega_lotto'

因为加载这个 web 类库是可选的,我们/不会/包含它在 lib/mega_lotto.rb 的入口文件中:

require "mega_lotto/version"
require "mega_lotto/drawing"
# notice that "mega_lotto/web" is not included here

module MegaLotto
end

真实世界的例子

Sidekiq gem 有一个关于不需要默认依赖的 web 库的绝佳的例子。你可以选择通过 require sidekiq/web 类库并且挂载包含的 Sinatra 应用到你的宿主应用中.

用来支持测试的类库是另一个常见的例子: 测试的 模块/类 可能是你的 gem 一部分,但是不是默认加载的。 Sidekiq 有一个测试类会直接运行你的后台脚本而不是异步执行。我的 gem Sucker Punch 有一个类似的测试库也不会被默认加载。

这两个例子中, 类库都没有被包含在入口文件并且留给用户去包含它们的 spec/spec_helper.rb 文件的权力,如果他们想要这个功能的话。

核心扩展

active_support gem 是一个 Rails 的依赖并且提供了成吨地价值。看一看 core_ext 目录。

这些是加入的扩展,它们看上去就像一个个 Ruby 标准库中的类。这些扩展让我们的生活更美好。比如这些方法:

5.seconds # => 5 seconds "asdf".present? # => true (1..42).to_a.forty_two # => 42

好吧,可能它们并不是每个都非常有用,但是很多是有用的。这些功能本可能被放在不用的类中,但是 Rails 核心团队选择扩展 Ruby 的默认行为,使得框架更容易适应新的开发者。 这就是为什么很多 Ruby 和 Rails 的新手会难以分辨哪些方法是 Rails 的,哪些是 Ruby 的。

Ruby 让扩展标准类库变得非常容易。比如,如果我们想要加入 #bananas 方法到 String 类中,只要这样做:

class String
  def bananas
    "bananas"
  end
end

。。。这就是一个核心扩展! 很简单, 是吧?

然而, 能力越大,责任越大。到处加入核心扩展不是一个好主意。事实上,我得说在大部分情况下,创建一个单独的类在我们的 gem 的命名空间下来加入额外的功能都是更好的选择。

假定我们发现没有更好的方法来包含扩展的方法在你的 gem 中,让我们讨论一下在我们的 gem 中使用 Ruby 的猴子补丁。

如果我们只加入 1 个或 2 个方法到 1 个或 2 个类中,通常采用的实践是创建一个叫做 core_ext.rb 的文件在我们的 gem 的 lib/ 目录下.

想象下我们正在开发 mega_lotto gem 并且我们想要加入 #draw 方法到 Array 类(不要真的这样做)。 #draw 方法将会选择一个随机元素在 array 中并且返回它。幸运的是,Ruby 已经有了一个方法来做这件事,所以我们的实现将会委托它。

# lib/mega_lotto/core_ext.rb

class Array
  def draw sample

  end
end

我们搞定了。

这个文件现在就像其他我们在 gem 中创建的文件一样。把它加入到主入口 lib/mega_lotto.rb 文件中,这样我们可以从 Array 类中访问到 draw 方法。

大量的方法

如果,由于某些原因,我们需要加入不止一些的方法到 Ruby 的核心类库,比较好的做法是把它们分离到一个单独的文件,每个文件对应它们要打的猴子补丁的类。

为了举例,我们将会把 #draw 方法加入上述的方法,并且一个新的方法 mega_lotto 在 String 类中将会从已有的字符串中打印一个随机的名字。

就像这样:

已经我们想要加入一个方法给 String 类并且另一个给 Array 类,让我们创建一个主 core_ext.rb 文件,它就做一件事,加载我们将会创建的独立的扩展类。

# lib/mega_lotto/core_ext.rb

require "mega_lotto/core_ext/array"
require "mega_lotto/core_ext/string"

#draw 方法的实现就和以前一样,仅仅是在不同的文件中而已:

# lib/mega_lotto/core_ext/array.rb

class Array
  def draw sample

  end
end

注意这个文件是如何放在 lib/mega_lotto/core_ext/ 目录中的。这是一个常用实践对于命名空间任何类库包含多个子类。 也注意下虽然我们的扩展是在 lib/mega_lotto/ 目录下,这个类不是一个 MegaLott 命名空间的派生。如果是的话,就会创建一个完全新的 String 类在我们的命名空间了,而不是 Ruby 标准库里的 String 类了。

这是我们的 mega_lotto 对应 String 类的实现:

# lib/mega_lotto/core_ext/string.rb

class String
  def mega_lotto
    "Mega Lotto: #{self}"
  end
end

由于我们的 lib/mega_lotto/core_ext.rb 文件已经完成了加载单独扩展的任务,主文件lib/mega_lotto.rb 可以再一次加载 lib/mega_lotto/core_ext.rb 文件。 lib/mega_lotto/core_ext.rb 文件可以刻刀所有它包含的库。 这就是当 gem 加载单独的类扩展时的文件结构看起来的样子:

真实世界的例子

在我的 sucker_punch gem 中, 我扩展了 String 类用一个下划线方法. 这是一个完美的例子 需要一个方法从 active_suppoert. 不是把整个 active_support gem 拽到我的项目中仅仅为了一个方法, 我选择复制了复制 underscore 方法的实现到一个单独的 core_ext.rb 文件. 像这样复制方法不总是明智的, 但是我很确定这个实现可以满足我的需求而且今后不需要升级.

Sidekiq gem也加入了扩展 Ruby 虽然这个文件中的代码是更复杂的, 但是用例对于任何 gem 来说都是一样的— 加入代码到标准 Ruby 类中来支持实现 gem 的代码.

总结

当考虑一个深层次的命名空间体系时很容易迷失在其中。当我创建一个新的 Ruby 类时,我倾向于也考虑一下它属于什么『bucket』。

这个『bucket』,在大在大多数情况下,可以翻译为 Ruby 命名空间和它所处的目录名称,因为它们通常是相关的。

本章提到的内容是有价值的因为它们不仅适用于 Ruby gems,也适用于任何 Ruby 应用只要有多于一层命名空间。

我喜欢规范的答案,但是不幸的是可预期的解决方案对于组织代码在一个程序中不总是存在的。通常来说,我喜欢在行动之前痛苦。 虽然,如果等待了太久,挖掘你的出路往往是更痛苦的。平衡复杂性和过度工程是所有开发者的挑战任务。你从你的决定中经历的痛苦越多,你就会在将来更好的避免这个问题。 并没有硬性规定。 Ruby 足够灵活来实现作为开发者的我们的需求。保持良好的组织和易懂性的责任在于我们。

如果我们想要开源我们的 gem 并且关注其他贡献者,从贡献者的角度考虑组织是明智的。无论你的 gem 有多酷,很少有人会愿意牺牲自己的时间来贡献给一团糟的代码。

在下一章,我们将会探索如何结合可执行命令到一个 gem。