构建一个 Ruby Gem 第四章 代码

代码

我们将会在本章实现我们的 mega_lotto 的核心代码. 然而, 在我们开始之前, 我想要花些时间来讨论一下 Ruby 的命名空间和从其他目录加载 class 时可能发生的情况.

让我们再次看看我们的 gem 的 API 风格:

MegaLotto::Drawing.new.draw # => [23, 22, 3, 7, 16]

命名空间

我不得不承认, 我建议从一个 Ruby gem 中学习命名空间是如何工作的. 因为我们给我们的gem命名为 mega_lotto, 相应的 Ruby 命名空间是 MegaLotto. 这是最佳实战, 来避免和其它类库的命名冲突.
假设名字 MegaLotto 是唯一的, 我们可以确定我们的代码不会和其它gem的代码冲突.

一般来说, 一个 Ruby 库的文件名对应一个相同根命名空间的模块/类的名字. 下面是几个例子:

lib/mega_lotto/store.rb => MegaLotto::Store
lib/mega_lotto/lucky_ticket.rb => MegaLotto::LuckyTicket

注意: 命名代码和模块是困难的. 我最好的建议是跟随单一职责原则并且创建只做一件事的文件/模块, 并且把一件事做好.

包含代码

记住, 默认情况下, bundler 为我们创建了 lib/mega_lotto.rb 文件.

require "mega_lotto/version"

module MegaLotto
  # Your code goes here...
end

其中, bundler 给我们创建了 MegaLotto 的命名空间并且建议我们的 gem 的代码应该写在这里面. 通常来说, 我们只会把配置和初始化代码放在这个文件里, 把具体实现放在 /lib/mega_lotto 目录中的其他类库中.

注意这行:

require "mega_lotto/version"

当我们使用 bundler 初始化我们的 Ruby gem 时, bundler 创建了文件 lib/mega_lotto/version.rb:

module MegaLotto 
  VERSION = "0.0.1"
end

文件 lib/mega_lotto/version.rb 文件定义了一个常量 (VERSION) 来标志我们的 gem 的版本. 当我们发布新版时, 我们会增加 prior 的值来推送我们的新代码到 Rubygems. 另外, 注意我们如何在文件 lib/mega_lotto/ 目录中使用相对路径来包含加载文件的.

实现


作为 Rubyists, 测试我们工作流程中必备的一部分. 测试不仅能保证我们的系统是可用的, 也保证了我们能写出可维护的代码. 我们知道我们的API的输入和输出, 所以我们可以从一个 spec 开始( spec/mega_lotto/drawing_spec.rb ) 来驱动我们的 Drawing 类的实现:

require "spec_helper"
module MegaLotto
describe Drawing do
    describe "#draw" do
        let(:draw) { MegaLotto::Drawing.new.draw }
        it "returns an array" do
            expect(draw).to be_a(Array)
        end
        it "returns an array with 6 elements" do
            expect(draw.size).to eq(6)
        end
        it "each element is an integer" do
            draw.each do |drawing|
                expect(drawing).to be_a(Integer)
            end
        end
        it "each element is less than 60" do
            draw.each do |drawing|
                expect(drawing).to be < 60
            end
        end
    end
end

简单来说: 这个 spec 断言 #draw 方法返回了一个 5 个整数组成的数组, 每一个整数都小于 60.

如果我们运行这个 spec, 我们会得到预期的错误:

/Users/bhilkert/Dropbox/code/mega_lotto/spec/mega_lotto/drawing_spec.rb:4:
in `<module:MegaLotto>`: uninitialized constant MegaLotto::Drawing (NameError)

迈着最小的步子来修复我们的失败 spec, 让我们在 lib/mega_lotto/drawing.rb 中创建 MegaLotto::Drawing 类 :

module MegaLotto 
  class Drawing 
  end
end

注意: 注意到 Drawing 类是如何在 MegaLotto 命名空间内部的? 文件 lib/mega_lotto.rb 定义了
MegaLotto 的根命名空间, 所以其它在 lib/mega_lotto/ 目录下的类库就会在MegaLotto命名空间下.


即使我们创建一个新的类 MegaLotto::Drawing, 我们的 gem 也还不知道它. gem里的文件不会被自动加载. 为了加载我们的 Drawing 类, 我们需要从 lib/mega_lotto.rb 这个入口文件中 require 它:

require "mega_lotto/version"
require "mega_lotto/drawing"

begin
    require "pry"
rescue LoadError
end

module MegaLotto
end

现在, 通过在宿主程序中包含 lib/mega_lotto.rb 入口文件, 我们可以得到在 lib/mega_lotto/drawing.rb 里的功能了!

If we re-run the spec for our Drawing class, we get the following:

如果我们重新运行 Drawing 类的 spec, 我们会得到下面的内容:

MegaLotto::Drawing
#draw
returns an array (FAILED - 1)
each element is less than 60 (FAILED - 2)
returns an array with 5 elements (FAILED - 3)
each element is an integer (FAILED - 4)
Failures:
1) MegaLotto::Drawing#draw returns an array
Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw }
NoMethodError:
undefined method `draw` for #<MegaLotto::Drawing:0x007fef4b0bf198>
# ./spec/mega_lotto/drawing_spec.rb:6
# ./spec/mega_lotto/drawing_spec.rb:9
2) MegaLotto::Drawing#draw each element is less than 60
Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw }
NoMethodError:
undefined method `draw` for #<MegaLotto::Drawing:0x007fef4b0bc510>
# ./spec/mega_lotto/drawing_spec.rb:6
# ./spec/mega_lotto/drawing_spec.rb:23
3) MegaLotto::Drawing#draw returns an array with 5 elements
Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw }
NoMethodError:
undefined method `draw` for #<MegaLotto::Drawing:0x007fef4b0b5fd0>
# ./spec/mega_lotto/drawing_spec.rb:6
# ./spec/mega_lotto/drawing_spec.rb:13
4) MegaLotto::Drawing#draw each element is an integer
Failure/Error: let(:draw) { MegaLotto::Drawing.new.draw }
NoMethodError:
undefined method `draw` for #<MegaLotto::Drawing:0x007fef4b0b4270>
# ./spec/mega_lotto/drawing_spec.rb:6
# ./spec/mega_lotto/drawing_spec.rb:17
Finished in 0.00218 seconds
4 examples, 4 failures

每个失败都是由于缺少一个 #drawing 方法, 所以让我们加上它:

module MegaLotto
    class Drawing
        def draw
        end
    end
end

我们的 specs 显示的信息不同了, 现在的失败原因是缺少返回的数组:

MegaLotto::Drawing
#draw
returns an array (FAILED - 1)
each element is an integer (FAILED - 2)
returns an array with 5 elements (FAILED - 3)
each element is less than 60 (FAILED - 4)
Failures:
1) MegaLotto::Drawing#draw returns an array
Failure/Error: expect(draw).to be_a(Array)
expected nil to be a kind of Array
# ./spec/mega_lotto/drawing_spec.rb:9
2) MegaLotto::Drawing#draw each element is an integer
Failure/Error: draw.each do |drawing|
NoMethodError:
undefined method `each` for nil:NilClass
# ./spec/mega_lotto/drawing_spec.rb:17
3) MegaLotto::Drawing#draw returns an array with 5 elements
Failure/Error: expect(draw.size).to eq(5)
NoMethodError:
undefined method `size` for nil:NilClass
# ./spec/mega_lotto/drawing_spec.rb:13
4) MegaLotto::Drawing#draw each element is less than 60
Failure/Error: draw.each do |drawing|
NoMethodError:
undefined method `each` for nil:NilClass
# ./spec/mega_lotto/drawing_spec.rb:23
Finished in 0.00181 seconds
4 examples, 4 failures

从第一个失败开始, 我们来给#draw方法返回一个空的数组, 再看看这会带来什么结果:

module MegaLotto
    class Drawing
        def draw
            []
        end
    end
end

我们的 specs 的结果:

MegaLotto::Drawing
#draw
returns an array
returns an array with 5 elements (FAILED - 1)
each element is less than 60
each element is an integer
Failures:
1) MegaLotto::Drawing#draw returns an array with 5 elements
Failure/Error: expect(draw.size).to eq(5)
expected: 5
got: 0
(compared using ==)
# ./spec/mega_lotto/drawing_spec.rb:13:
in `block (3 levels) in <module:MegaLotto>`
Finished in 0.00773 seconds
4 examples, 1 failure

已知我们通过返回一个空的数组加入了一个无条理的实现, 让我们通过返回一个5个元素的数组满足最后一个失败:

module MegaLotto
    class Drawing
        def draw
            Array.new(5)
        end
    end
end

我们的 spec 失败现在产生了特定的关于数组的元素:

MegaLotto::Drawing
#draw
each element is less than 60 (FAILED - 1)
each element is an integer (FAILED - 2)
returns an array
returns an array with 5 elements
Failures:
1) MegaLotto::Drawing#draw each element is less than 60
Failure/Error: expect(drawing).to be < 60
NoMethodError:
undefined method `<` for nil:NilClass
# ./spec/mega_lotto/drawing_spec.rb:24
# ./spec/mega_lotto/drawing_spec.rb:23:in `each`
# ./spec/mega_lotto/drawing_spec.rb:23
2) MegaLotto::Drawing#draw each element is an integer
Failure/Error: expect(drawing).to be_a(Integer)
expected nil to be a kind of Integer
# ./spec/mega_lotto/drawing_spec.rb:18
# ./spec/mega_lotto/drawing_spec.rb:17:in `each`
# ./spec/mega_lotto/drawing_spec.rb:17
Finished in 0.00417 seconds
4 examples, 2 failures

现在让我们返回0到60之间的数组而不是nil:

module MegaLotto
    class Drawing
        def draw
            5.times.map { single_draw }
        end
        private
        def single_draw
            rand(0...60)
        end
    end
end

现在看看我们的 specs, 可以看到我们满足了要求:

MegaLotto::Drawing
#draw
each element is less than 60
returns an array
returns an array with 5 elements
each element is an integer
Finished in 0.00551 seconds
4 examples, 0 failures

万岁! 让我们提交我们的变更并且庆祝我们的新 gem。 我们离发布更近了一步。

总结

驱动测试开发是一个很多 Rubyist 遵照的实践。如果没有测试的设置,先写下你的测试通常会产出高质量可维护的代码。我给了你我上面的工作流的感觉,但是不是所有的开发者都是一样的。实验你的工作流并且确定什么样的适合你。虽然测试在开始花费额外的时间,但这样的投资节省了让我的头疼的麻烦。我鼓励你尝试它如果你没实践过TDD。

在下一章, 我们将会看看 bundler 提供的 rake 任务是如何来帮助发布的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容