構建一個 Ruby Gem 第二章 結構

當我第一次開始制作我自己的 Ruby gems 的時候,最讓我沮喪的事情之一就是理解文件結構的約定。即使我可以讓一些代碼在一起工作,我想要真正理解到底發生了什么和相關的最佳實踐。

我發現的一些最好的資源是在 RubyGems.org 上(沒想到吧……)。他們的手冊寫的很好并且值得一讀的如果你想嚴肅地構建 gems。

命名

當構建一個 ruby gem 時一件值得考慮的事情是它的名字。隨著越來越多的 gem 被創建,找到一個獨一無二的并且有意義的名字就越來越難。我偏好代表功能的名字。然而,名字取決于我們。只要我們的 gem 有一個自己的 README (我們很快會涉及)并且我們能精確描述它做了什么和如何使用它,名字就不是那么重要了。

另一個需要考慮的點是當選擇一個 gem 的名字時,如何才能谷歌到它。選擇一個普通的名字比如 graph 或者 statistics 將會很難被谷歌到。一個很好的檢驗方式是搜索目標名字加上 "ruby" 或者 "ruby gem"。

另一件值得做的事情是快速的搜索一下 Rubygems.orgGithub 來確保沒有人用過這個名字。我就預先確保了 mega_lotto 這個名字沒有被人使用過。這年頭,就像搜索域名一樣 — 所有的好名字都被搶了,或者說我們需要更多的創造力!

Rubygems.org 有一個絕佳的關于命名規范的手冊. 花幾分鐘來閱讀它(篇幅很短). 我在這里等你...

插入你最喜歡的電梯音樂。

好了,現在你已經在 Ruby gem 的世界中接受過完整的教育了,我要強調一件事:

除非你知道你在做什么,使用下劃線而不是中劃線。

中劃線的常用意義是創建一個已有gem的擴展。比如:postmark-rails 是給 rails 加上郵戳,premailer-rails 給 rails 加上 premailer。這些 gems 都使用中劃線在它們的名字中表示它們是 rails 的擴展。

你大概注意到了一些 gem 使用中劃線但是不是已有 gem 的擴展。可能有兩個原因,一是這個 gem 是很多年前被創建的,那時標準不太清晰,或者他們沒有意識到命名的最佳實踐,本可以閱讀一下上面的手冊。

保持名字和社區的約定一致。就會減少混亂,這從根本上是一件好事,因為我們是那個在發布 gem 后對可能造成的混亂負責的人.

如果你發現了一個相同名字的 gem,給社區一個面子然后選擇一個別的名字。否則不僅會讓那些使用已存在的 gem 的人困惑,也會讓使用你的 gem 的人困惑。我以前走過這條路,結局是讓人沮喪的。

Bundler


事實證明 bundler 包含的功能比大多數人意識到的要多。除了使用它來安裝一個應用程序的 gems 以外 (bundle install),Bundler 也有內建的命令來幫助你管理創建和維護一個 ruby gem 的過程。

$ bundle help
...
bundle gem(1)
Create a simple gem, suitable for development with bundler
...

所以我們還在等什么呢?讓我們創建我們的一個第一個 Ruby gem 吧!我們將會使用 bundle gem 命令來啟動我們的 MeagLotto gem 的初始化代碼。

$ bundle gem mega_loto 

注意:因為我之前已經發布了 mega_lotto 這個 gem 到 Rubygems 了,如果你使用相同的 gem 的名字就不能再發布了,一個選擇是改變 gem 的名字 (比如 brandons_mega_lotto ),或者跳過發布的步驟。

讓我們看看我們都創建了什么:

    .
    ├── Gemfile
    ├── LICENSE.txt
    ├── README.md
    ├── Rakefile
    ├── lib
    │   ├── mega_lotto
    │   │   └── version.rb
    │   └── mega_lotto.rb
    └── mega_lotto.gemspec
    2 directories, 7 files

有很多需要注意的事情……有一個 lib 目錄包含了一個文件 (mega_lotto.rb) 和一個目錄(lib/mega_lotto/)都叫做 "mega_lotto"。這和 Rubygems.org 關于文件結構的手冊上是一致的:

你打包的代碼位于 lib 目錄。依照慣例應該有一個和你的 gem 名字一樣的 Ruby 文件,因為當別的程序 require 'mega_lotto' 時,這個文件會被加載。這個同名文件負責設定你的 gem 的代碼和 API 。

考慮到入口文件 ( lib/mega_lotto.rb ) 的工作是加載 gem 的依賴. 這些依賴可能是內部或者第三方的庫. 而內部的類, 就像 Rubygems 建議的那樣, 應該放在 lib/mega_lotto/ 目錄下并且從那里被引用.

默認文件

gem的根目錄看起來就像這樣:

    ├── Gemfile
    ├── LICENSE.txt
    ├── README.md
    ├── Rakefile
    ├── lib
    │   ├── mega_lotto
    │   │   └── version.rb
    │   └── mega_lotto.rb
    └── mega_lotto.gemspec

許可證

上面輸出的那個 LICENSE.txt 文件默認是 MIT 許可證。它規定代碼可以被任何人用于做任何事而不用額外的許可或者認可。大多數開源項目使用這個許可證,因此 bundler 默認就用它。

選擇一個許可證可以是很麻煩的。我敢保證有很多軟件工程師在(或者曾經在)大型組織中花上數小時來要求公司的法律部門審批對于開源軟件的使用請求。無論你的態度如何,許可證是一件要緊的事情。如果默認的 MIT 許可證不適合你的項目,看看 http://choosealicense.com/。它提供了一份常見軟件許可證的指導手冊。如果你還是有疑問,那就需要聯系一個律師或者某個熟悉軟件許可證的人去向他學習了。

Readme

README.md 文件是我們的gem的文檔. 一個 README 文件的最簡形式, 需要至少能夠回答下面幾個問題:

  1. 它是什么
  2. 我如何使用它
  3. 我如何作貢獻

額外的部分比如系統要求, 安裝, 作者, 貢獻者和許可證可以在很多項目里被發現. 通常來說, 沒人會責怪你在 REAME 里寫太多的東西。

幸運的是,bundler 生成的 README 文件已經包含了這些部分。我們所要做的就是適當的填上這些空白。
大多數項目使用 markdown 文件格式(因為 .md 后綴)。

如果你的項目的 README 開始變得非常的長并且host在github上, 那么github的項目wiki就是你的下一步選擇.
如果我們特意使用了wiki, 我們要把這件事記錄在README上, 這樣用戶們才會知道要去查看wiki. 另外一個使用項目wiki的好處是, 我們(作為項目擁有者), 可以允許其他人作貢獻. 所以與其被更新文檔的pull request淹沒, 我們可以建議那些用戶在必要的地方更新/創建wiki頁面.
這替代了一部分社區的職責, 這是合理的因為我們正在創建免費軟件供他們使用. 如果沒有社區的出錢出力我們也不會有開源軟件.

Rakefile

Rakefile 是一個用來定義和組織任務的文件,我們會從命令行運行它。默認情況下,它包含下面的內容:

require "bundler/gem_tasks"

通過引用這個庫,bundler 提供我們一些內置的任務來發布我們的gem。我們會在適當的時候更詳細的了解它們。

Gemspec

最后, gem spec (mega_lotto.gemspec)... 只有很少的字段需要更新因為bundler已經為我們做了剩下的事情.

# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'mega_lotto/version'
Gem::Specification.new do |spec|
  spec.name = "mega_lotto"
  spec.version = MegaLotto::VERSION
  spec.authors = ["Brandon Hilkert"]
  spec.email = ["brandonhilkert@gmail.com"]
  spec.description = %q{TODO: Write a gem description}
  spec.summary = %q{TODO: Write a gem summary}
  spec.homepage = ""
  spec.license = "MIT"
  spec.files = `git ls-files`.split($/)
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
  spec.require_paths = ["lib"]
  spec.add_development_dependency "bundler", "~> 1.3"
  spec.add_development_dependency "rake"
end

一開始的兩行把我們的 gem 的 lib 目錄加入了 load path (ruby會尋找的額外庫的path)。這會允許我們從我們的宿主應用調用 "mega_lotto"并且讓它正確的加載我們的gem。

規范的第一部分是一些關于我們(作為作者)和gem的元數據。在發布之前,我們要完成描述,總結和主頁。之后,我們會看到這些信息被用在 gem 的 Rubygem.org 的頁面上。

中間部分使用 git 來確定我們的 gem 的文件。有一點要注意,如果你更改你的 gem 并且通過本地文件路徑引用你的 gem 的應用程序進行測試時,一些文件可能是不可見的如果它們沒有被 git 提交過(比如 可執行的命令行文件)。

最后一部分是我們的gem的依賴定義。當在宿主應用中被調用時,bundler 將會和我們的 gem 的代碼一起安裝這些依賴。要注意有兩種依賴方法。

  • add_development_dependency — 定義一個開發環境依賴。當被用于生產環境或者當開發我們的宿主程序時,這些依賴不會被安裝;只有在開發本地 gem 時才會被安裝。
  • add_dependency — 定義在所有環境中都需要的依賴。

雖然 bundler 在我們的項目的根目錄創建了一個 Gemfile,它不應該被修改除非我們有特殊的理由這么做。

source 'https://rubygems.org'
# Specify your gem's dependencies in mega_lotto.gemspec
gemspec

gemspec方法表明了bundler會從 mega_lotto.gemspec 文件去定義 gem 的依賴。

README 驅動開發


這是一個真實的故事. Tom Preston-Werner 發帖說明了 REAME 驅動開發的好處,Zach Holman,也做了一個相關的分享

如果我們想要我的 gem 被其他人使用,我們應該為它寫一個README。如果這不能說服你,我可以告訴你無數次當我寫代碼,隔了幾天之后就忘了代碼在做什么了(這從來也沒發生在你的身上, 可能嗎?)。因此,一個包含了代碼用例的 README 就會很好的為我們服務。下面是我認為我們要先寫 README 的理由:

  1. 它迫使我們預先定義公共 API (宿主應用可以調用的方法),這會減少我們過程工程的傾向。
  2. 它把煩人的文檔部分放到了前面。當你完成代碼并且要發布gem時,最后要做的是撰寫文檔。說真的,這一點也不好玩。所以如果我們可以把這件事挪到前面來做,我們的處境就會好些。
  3. 我們會忘記我的 gem 是如何工作的…… 所以如果我們想要其他人來使用它,我們應該寫一個 README 來說明安裝,用法和如何做貢獻(如果我們歡迎貢獻者)。

希望這說服了你一個README 是重要的。雖然我認為如果你在開發之前就寫 README 會比較好,但是我知道每個人的工作方式都是不同的。更大更重要的點是你的項目要有一個 README。

公共 API

經常的, 很容易迷失在去寫孤立的殺手級代碼,但是記住我們的 gem 的重心是封裝會被外在的宿主應用調用的邏輯和功能。我們要讓 gem 的方法能被用簡單直接的方式調用。

以 mega_lotto 為例, 這是我們期望的目標:

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

注意:如果你正在寫一個不同的 gem,想象為你的 gem 提交一個公共 API。從一開始就牢記這一點就會減少你代碼的混亂并且讓你聚焦在實現你需求的功能的部分。

總結

Bundler 搞定了樣板代碼,讓我們把注意力放在我們的 gem 的價值和編碼上,我們花了很多時間復習一個常規 gem 的文件結構。一旦我們熟悉并且適應了這樣的文件結構,我認為你會在其他地方發現它的價值。把代碼都組織在一個 Rails 應用的 lib 目錄下并沒有壞處。事實上,這就是抽取功能到一個獨立的 gem 的前身,我們會在本書的后續討論。這也是一個很好的方法來保持 Ruby 類的組織和命名空間。我也發現按照這樣的文件結構導致我更多的考慮類的名字。結果是,我的代碼有著更好的組織結構并且獨立(贊一下單一職責原則)。

在下一章,我們會看看如何設置 Rspec 和其他 debug 工具來幫助我們的開發過程。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容