從我們在Global Personals專案中使用Github並且following Github Flow開始到現在已經將近兩年的時間。在這段時間中,我們以很高的頻率提交了上千次的pull請求,雖然沒有太多如何改善或提高程式的建議和想法,但是我仍獲得瞭如此廣泛和珍貴的經驗。其中,有一些建議是和專案相關的,同時,也包含了大量可以在團隊內分享的Ruby開發小技巧。
由於我擔心將從這個專案中獲得和學習到的如此珍貴的技巧和經驗所遺忘,於是我挑出了其中最好的最有價值的部分和大家分享,同時進行了一點小小的擴充套件。每個人都有自己的工作方式和風格,所以我會簡潔明瞭地和大家闡述。並不是每部分內容對每個人來說都是新的,但是希望你在這裡可以或多或少都有所收穫。
這是Ruby小技巧系列的第四部分,這些技巧是我們從過去兩年的實戰經驗中所收穫的。
- 第一部分涵蓋了程式碼塊(blocks)和範圍物件(ranges),
- 第二部分討論了拆分重構以(destructuring)及型別轉換(type conversions),
- 第三部分討論了異常(exceptions)和模組(modules)。
除錯(Debugging)
Rails控制檯(Rails console)對於互動式除錯非常有用,這種方式對於非rails應用來說也是非常方便的。通過stdlib中的irb,讓你的專案啟動並執行變得出奇的簡單。
這個例子假設你將程式碼放到了lib/
目錄下,並且你的專案明確指出可以通過require
載入所有程式碼。
我增加了一個設定Sequel資料庫連線的例子,你可以使用你需要的初始化設定的程式碼去替換它,或者僅僅是將它移除。如果你不使用Bundler,你也可以移除require "bundler/setup"
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#!/usr/bin/env ruby require "bundler/setup" require_relative "lib/my_project" require "sequel" require "irb" def config return @config if @config config_path = File.expand_path("../config/application.yml", __FILE__) @config = YAML.load_file(config_path) end DB = Sequel.connect(config[:database]) IRB.start |
將上面的程式碼儲存到專案的根目錄並命名為console
,同時在命令列輸入chmod +x console
使檔案變為可執行的。現在,你可以通過執行專案根目錄下的./console
檔案來開啟一個控制檯。
任何在IRB.start之前宣告的方法,例項變數,以及常量在整個控制檯會話期間都是可用的。但是它會建立一個新的作用域,因此區域性變數是不可用的。之後的語句將會在退出時執行。
當你在控制檯除錯時,irb將輸出在每個表示式值執行結果上呼叫方法#inspect
的結果。通過這會給你一個美觀的,低層次的展現方法,這對除錯非常有用。然而,對於浮點數(Floats)這並不會十分有用。由於Ruby(以及大多數程式語言)需要根據你的指定來顯示精確的值,因此,一種顯示浮點數精確的值的方式是使用sprintf
1 |
sprintf("%.50f", 1.115) #=> "1.11499999999999999111821580299874767661094665527344" |
其他的一些時候,你會遇到相反的問題,#inspect
的輸出結果對於你所期望的過於模糊。
1 2 3 4 5 |
require "bigdecimal" decimals = [BigDecimal("1.0")] 5.times {decimals << decimals.last / 2} decimals #=> [#<BigDecimal:7fbdb8871928,'0.1E1',9(18)>, #<BigDecimal:7fbdb88717c0,'0.5E0',9(36)>, #<BigDecimal:7fbdb88716a8,'0.25E0',9(36)>, #<BigDecimal:7fbdb8871590,'0.125E0',9(36)>, #<BigDecimal:7fbdb88714a0,'0.625E-1',9(36)>, #<BigDecimal:7fbdb8871360,'0.3125E-1',9(36)>] |
在這種情況下,你可以在當前的測試中或控制檯指令碼中重新定義類的#inspect
例項方法來返回一些更加直觀的內容。
1 2 3 4 5 6 7 |
class BigDecimal def inspect "#{to_s("F")}d" end end decimals #=> [1.0d, 0.5d, 0.25d, 0.125d, 0.0625d, 0.03125d] |
出於同樣的原因,在你自己的類中實現#inspect
方法也是非常有幫助的。
有時候你想要除錯一個bug,但是卻得到兩個非常相似的輸出結果。你肯定不願意通過輸出的結果自己尋找bug。但是,如果你把每個輸入結果儲存到檔案中,使用diff
命令檢視又是非常費勁的,尤其是當你做連續的修改,邊執行邊除錯的時候。幸運地,在Ruby標準庫中有一個diff
方法可以方便的進行操作。
1 2 3 4 5 6 7 |
require "minitest/unit" include Minitest::Assertions a = ["foo", "bar", "baz"] b = ["food","bar", "baz"] puts diff(a.join("\n"), b.join("\n")) |
輸出:
1 2 3 4 5 6 7 |
--- expected +++ actual <a href='http://www.jobbole.com/members/li754132448'>@@</a> -1,3 +1,3 <a href='http://www.jobbole.com/members/li754132448'>@@</a> -"foo +"food bar baz" |
作為一種高度動態語言,可以允許你在任何時間改變任何東西,有時候在Ruby中找到一個方法真正定義的地方是非常令人迷惑的,Ruby可以為你記錄這些資訊。
1 2 3 4 5 6 |
require "set" array = [1,2,3] m = array.method(:to_set) # get ahold of an object representing the to_set method m.owner #=> Enumerable m.source_location #=> ["~/.rbenv/versions/2.0.0-p247/lib/ruby/2.0.0/set.rb", 635] |
上面的結果顯示,Array例項中的#to_set
方法來自Enumerable模組,但是它被真正定義的地方是在標準庫(stdlib)中set.rb
檔案中的第635行。
專案佈局(Project Layout)
Ruby總沒有統一的方式構建程式的佈局,但是如果你將程式碼按gem包的方式構建將會是一種很好的方式,同時也便於其他專案使用。
RubyGems將每一個gem的資料夾路徑增加到載入路徑(load path)中。你不會希望這個路徑成為專案的根路徑,由於你希望其他的一些附屬檔案(Gemfile, tests, build scripts等)在gem對於其他的使用者是可用的。在這種情況下,通常將你的程式碼放到lib
資料夾下,並且在gemspec檔案中設定require_path
指向lib
。
現在,在lib
資料夾中的檔案都可以通過require "name"
來載入。這通常工作的很好,對於核心類和模組。但是假設你有一個名為api.rb
的檔案,這是一個常見的命名有可能和其他的gem或者其他使用者的程式碼發生衝突。由於這個原因,一個好的做法是在lib
資料夾下建立一個單獨的主檔案,然後再將其他的程式碼放到以主檔名稱命名的資料夾中。
其他一些附屬的檔案可以放到根目錄下,同時大多數人也會在根目錄下建立test
(spec
)目錄。
1 2 3 4 5 6 7 8 9 10 11 12 |
my_project/ lib/ my_project/ bar.rb foo.rb my_project.rb test/ my_project_test.rb console Gemfile my_project.gemspec Rakefile |
通常你會每個類使用一個檔案,以類的名字命名檔案,以下劃線的方式而不使用駝峰式。在lib
下的資料夾和名稱空間相匹配,因此檔案lib/my_project/foo.rb
中的內容結構如下:
1 2 3 4 5 |
module MyProject class Foo # ... end end |
我經常會有一些檔案不會直接對映到一個類,檔案lib/my_project/errors.rb
包含專案所有的異常類,檔案lib/my_project/constants.rb
包含專案所有的常量。
沒有必要嚴格遵守 “一個檔案一個類的規則”,如果你只是有一些小的幫助類被另外一個所使用,完全可以將它們放在一起。
當你在專案中包含檔案時,你需要使用require_relative
。require
是一種載入gems以及標準庫(stdlib)檔案的很好的方式,但是它只會從全域性變數$:
($LOAD_PATH
)搜尋檔案,你並不能明確要載入的檔案。require_relative
總是從當前檔案所在的目錄開始搜尋檔案,不管怎樣總是能找到相同的檔案。在測試中非常有用,你總是能明確你測試的是哪些程式碼。
通常一種很好的做法是在lib/my_project.rb
檔案中載入所有的類,至少是核心類。通過這種方式,如果將程式碼帶包為一個gem或者寫測試,指令碼等的時候,可以通過一個“入口檔案”載入所有的類。
文件(Documentation)
Ruby包含豐富的文件,還有一個好用的搜尋小工具ri
。它的使用非常簡單,只需要在命令列執行ri
。當它執行之後,輸入一個類名之後,你可以得到一個類的概要介紹和方法列表。你可以輸入ClassName.method
檢視類方法,輸入ClassName#method
檢視例項方法。
所有安裝的gem都可以通過ri檢視文件,如果它的作者撰寫了相關文件。
你也可以執行gem server
,通過http://0.0.0.0:8808檢視gem的文件。
你可以使用rdoc
來為你自己的程式碼構建文件,只需要將程式碼的資料夾和一些附加的檔案傳遞給rdoc
。它將輸出一個doc
目錄,使用瀏覽器開啟doc/index.html
即可看到生成的內容。
1 |
rdoc lib README.rdoc --main README.rdoc |
像上面一樣會生成類和方法的概述,但是你可以生成對別人更加有用的文件,通過使用rdoc格式的註釋。下面的這些註釋,rdoc將輸出為類和方法的描述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
module Namespace # :nodoc: so we don't have an empty doc page for the namespace # The Example class is an example of rdoc # # Link to a real # example[https://github.com/globaldev/going_postal/blob/master/lib/going_postal.rb] #-- # double minus hides docs from here on, we can un-hide with double plus #++ # # = Heading # # == Sub Heading # # Indent example code 2 spaces (3 total from the #) # # example = Example.new # baz = example.foo(bar) # class Example # :call-seq: Example.new(arg) -> example # #-- # The :call-seq: directive lets you specify a custom example of how the # method is called, if you don't provide one the method name and arguments # are taken from the definition, and no return value is specified. #++ # # This is the initialize method, it gets documented as the +new+ class # method. # # Returns a new Example instance. # def initialize(arg) end # :section: demonstration methods # :call-seq: example.foo(bar) -> baz # # foos the bar, returning baz # def foo(bar) end # :call-seq: # example.qux(params) -> array or nil # example.quux(params) -> array or nil # # Available params are # [foos] an array of Foos # [bar] a Bar instance # [baz] a Baz instance (optional) # def qux(params) end alias quux qux # :section: block methods # :call-seq: example.each {|foo| block} -> example def each yield foo self end end end |
下一部分很快會和大家見面……