Skip to content

Latest commit

 

History

History
4134 lines (3229 loc) · 102 KB

README-zhTW.md

File metadata and controls

4134 lines (3229 loc) · 102 KB

序幕

榜樣很重要。
——墨菲警官《機器戰警》

作為 Ruby 開發者,有一件總是令我煩心的事 — Python 開發者有一份好的程式風格參考指南(PEP-8) 而我們永遠沒有一份官方指南,一份記錄 Ruby 程式風格及最佳實踐的指南。而我們確信風格很重要。我也相信這些好傢伙們,像我們是 Ruby 開發者,應該可以自己產生一份這夢寐以求的文件。

這份指南開始是作為我們公司內部 Ruby 程式指南(由我所寫的)。進行到某個部分時,我決定要把我的成果貢獻給廣大的 Ruby 社群,而且這個世界需要來自另一個公司內部的一點幫助。然而這個世界也可以從由社群制定及驅動的一系列 Ruby 程式慣例、實踐及風格中受益。

在開始寫這份指南時,我收到世界上很多優秀 Ruby 社群用戶們的反饋。感謝所有的建議及幫助!我們同心協力創造一個能夠讓每一個 Ruby 開發者受益的資源。

順道一提,如果你對 Rails 有興趣,你可以看看這份與之互補的 Ruby on Rails 風格指南

Ruby 風格指南

這份 Ruby 風格指南向你推薦現實世界中的最佳實踐,Ruby 程式設計師如何寫出可被別的 Ruby 程式設計師維護的程式碼。一份風格指南反映出現實世界中的用法,並帶有一個理想,避免已經公認是危險的事物被人繼續使用 –– 不管看起來是多麼的好。

本指南依照相關規則分成數個小節。我試著在每個規則後面說明理由(如果省略的話,我相信理由是顯而易見的)。

我沒有想到所有的規則 — 他們大致上是基於,我作為一個專業軟體工程師的廣泛生涯,從 Ruby 社群成員所得到的反饋及建議,和數個高度評價的 Ruby 程式設計資源,像是 "Programming Ruby 1.9" 以及 "The Ruby Programming Language"

本指南仍在進行完善中 –– 某些規則缺乏實例,某些規則沒有例子來清楚地展示它們。在完稿時,將會解決這些議題 — 現在就先把他們記在心理吧。

你可以使用 Pandoc 來產生本指南的一份 PDF 或 HTML 副本。

RuboCop 專案會自動檢查你的 Ruby 程式碼是否符合這份 Ruby 風格指南。目前這個專案尚有許多功能缺漏,不足以被正式地使用,歡迎有志之士協助改進。

本指南被翻譯成下列語言:

目錄

原始碼排版

幾乎每人都深信,每一個除了自己的風格都又醜又難讀。把 "除了自己的" 拿掉,他們或許是對的...
-- Jerry Coffin (論縮排)

  • 使用 UTF-8 作為原始檔案的編碼。 [link]

  • 每個縮排層級使用兩個空格。不要使用 Hard Tabs。 [link]

    # 不好 - 四個空格
    def some_method
        do_something
    end
    
    # 好
    def some_method
      do_something
    end
  • 使用 Unix 風格的行編碼(BSD/Solaris/Linux/OSX 的使用者不用擔心,Windows 使用者要特別小心。) [link]

    • 如果你使用 Git,你也許會想加入下面這個設定,避免專案被 Windows 的行編碼入侵:

      $ git config --global core.autocrlf true
  • 不要使用 ; 分隔敘述與表達式。 由此推論 - 一行一個表達式。 [link]

    # 不好
    puts 'foobar'; # 多餘的分號
    
    puts 'foo'; puts 'bar' # 同一行有兩個表達式
    
    # 好
    puts 'foobar'
    
    puts 'foo'
    puts 'bar'
    
    puts 'foo', 'bar' # 僅對 puts 適用
  • 沒有內容的類別,偏好使用單行定義。 [link]

    # 不好
    class FooError < StandardError
    end
    
    # 還可以
    class FooError < StandardError; end
    
    # 好
    FooError = Class.new(StandardError)
  • 避免單行方法。 雖然這種方式蠻普遍的,但是因為有點怪異的語法,使用起來並不好用。 無論如何 - 單行方法不應該有一個以上的表達式。 [link]

    # 不好
    def too_much; something; something_else; end
    
    # 還可以 - 注意第一個 ; 是必要的
    def no_braces_method; body end
    
    # 還可以 - 注意第二個 ; 是可選的
    def no_braces_method; body; end
    
    # 還可以 - 語法正確, 但沒有 ; 難以閱讀
    def some_method() body end
    
    # 好
    def some_method
      body
    end

    唯一的例外是沒有內容的方法

    # 好
    def no_op; end
  • 使用空格來圍繞運算元,逗點 , 、冒號 : 及分號 ; 之後,圍繞 {} 之前。 空格可能對(大部分)Ruby 直譯器來說是無關緊要的,但正確的使用是寫出可讀性高的程式碼的關鍵。 [link]

    sum = 1 + 2
    a, b = 1, 2
    class FooError < StandardError; end

    有幾個例外狀況,其中一個是使用指數運算子時:

    # 不好
    e = M * c ** 2
    
    # 好
    e = M * c**2

    另一個例外是在有理數中的分線(slash):

    # 不好
    o_scale = 1 / 48r
    
    # 好
    o_scale = 1/48r

    最後一個例外狀況則是安全運算子(safe navigation operator):

    # 不好
    foo &. bar
    foo &.bar
    foo&. bar
    
    # 好
    foo&.bar
  • 不要有空格在 ([ 之後,或 ]) 之前。 在 {} 的前後加上空格。 [link]

    # bad
    some( arg ).other
    [ 1, 2, 3 ].each{|e| puts e}
    
    # good
    some(arg).other
    [1, 2, 3].each { |e| puts e }

    由於 {} 被用在區塊與雜湊的語法,以及嵌入字串的表達式裡,應該要被更清晰的表達。

    對於雜湊的語法,可以接受下列兩種撰寫方式。 第一種寫法稍微容易閱讀(一般來說在 Ruby 社群裡也較為普及)。第二種寫法的優點是能夠在視覺上區分區塊與雜湊字面語法。不管你選擇哪一種方式——持續使用它。

    # 好 - 在 { 之後與 } 之前加上空格
    { one: 1, two: 2 }
    
    # 好 - 在 { 之後與 } 之前不加空格
    {one: 1, two: 2}

    至於嵌入的表達式則不在大括號中加入空格。

    # 不好
    "From: #{ user.first_name }, #{ user.last_name }"
    
    # 好
    "From: #{user.first_name}, #{user.last_name}"
  • 不要有空格在 ! 之後. [link]

    # 不好
    ! something
    
    # 好
    !something
  • 範圍的字面語法中不要有空格 [link]

    # 不好
    1 .. 3
    'a' ... 'z'
    
    # 好
    1..3
    'a'...'z'
  • whencase 縮排在同一層。我知道很多人不同意這一點,但這是 "The Ruby Programming Language" 及 "Programming Ruby" 所設立的風格。 [link]

    # 不好
    case
      when song.name == 'Misty'
        puts 'Not again!'
      when song.duration > 120
        puts 'Too long!'
      when Time.now.hour > 21
        puts "It's too late"
      else
        song.play
    end
    
    # 好
    case
    when song.name == 'Misty'
      puts 'Not again!'
    when song.duration > 120
      puts 'Too long!'
    when Time.now.hour > 21
      puts "It's too late"
    else
      song.play
    end
  • 當條件表達式的結果被賦值給一個變數時,保持分支的對齊。 [link]

    # 不好 - 非常複雜
    kind = case year
    when 1850..1889 then 'Blues'
    when 1890..1909 then 'Ragtime'
    when 1910..1929 then 'New Orleans Jazz'
    when 1930..1939 then 'Swing'
    when 1940..1950 then 'Bebop'
    else 'Jazz'
    end
    
    result = if some_cond
      calc_something
    else
      calc_something_else
    end
    
    # 好 - 清楚暸解做什麼事
    kind = case year
           when 1850..1889 then 'Blues'
           when 1890..1909 then 'Ragtime'
           when 1910..1929 then 'New Orleans Jazz'
           when 1930..1939 then 'Swing'
           when 1940..1950 then 'Bebop'
           else 'Jazz'
           end
    
    result = if some_cond
               calc_something
             else
               calc_something_else
             end
    
    # 好 (稍微減少行寬)
    kind =
      case year
      when 1850..1889 then 'Blues'
      when 1890..1909 then 'Ragtime'
      when 1910..1929 then 'New Orleans Jazz'
      when 1930..1939 then 'Swing'
      when 1940..1950 then 'Bebop'
      else 'Jazz'
      end
    
    result =
      if some_cond
        calc_something
      else
        calc_something_else
      end
  • def 之間使用空行,並且把方法分成合乎邏輯的段落。 [link]

    def some_method
      data = initialize(options)
    
      data.manipulate!
    
      data.result
    end
    
    def some_method
      result
    end
  • 不要在同一處中使用多個空行。 [link]

    # 不好 - 兩個空行
    some_method
    
    
    some_method
    
    # 好
    some_method
    
    some_method
  • 使用空行圍繞存取修飾符(Access modifier)。 [link]

    # 不好
    class Foo
      attr_reader :foo
      def foo
        # do something...
      end
    end
    
    # 好
    class Foo
      attr_reader :foo
    
      def foo
        # do something...
      end
    end
  • 不要用空行圍繞方法、類別、模組、程式碼區塊。 [link]

    # 不好
    class Foo
    
      def foo
    
        begin
    
          do_something do
    
            something
    
          end
    
        rescue
    
          something
    
        end
    
      end
    
    end
    
    # 好
    class Foo
      def foo
        begin
          do_something do
            something
          end
        rescue
          something
        end
      end
    end
  • 避免在方法最後一個參數之後加逗號,尤其是參數沒有分佈在不同行的時候。 [link]

    # 不好 - 比較容易移動/新增/移除參數,但還是不偏好這種做法
    some_method(
      size,
      count,
      color,
    )
    
    # 不好
    some_method(size, count, color, )
    
    # 好
    some_method(size, count, color)
  • 當賦予預設值給方法參數時,使用空格圍繞 = 運算元: [link]

    # 不好
    def some_method(arg1=:default, arg2=nil, arg3=[])
      # do something...
    end
    
    # 好
    def some_method(arg1 = :default, arg2 = nil, arg3 = [])
      # do something...
    end

    雖然幾本Ruby的書籍建議第一種風格,不過第二種風格在實務上更為常見(並應該會較容易閱讀)。

  • 避免在不需要的時候使用續行 \。在實務上,除了字串的連接,避免使用續行在任何地方。 [link]

    # 不好
    result = 1 - \
             2
    
    # 好 (但仍然醜到爆)
    result = 1 \
             - 2
    
    long_string = 'First part of the long string' \
                ' and second part of the long string'
  • 採用一貫的多行方法鏈結風格。在Ruby社群中有兩種普遍的風格,兩種都被認為是好的寫法 - 前置 .(選項 A) 與後置 . (選項 B)。 [link]

    • (選項 A) 當另一行繼續調用鏈結式方法時,把 . 放在第二行。

      # 不好 - 必須要查看第一行才能了解第二行
      one.two.three.
        four
      
      # 好 - 第二行發生什麼事可以立即地了解
      one.two.three
        .four
    • (選項 B) 當另一行繼續調用鏈結式方法時,將 . 放在第一行,暗示下一行還有表達式。

      # 不好 - 需要讀到第二行,才知道方法有繼續鏈結
      one.two.three
        .four
      
      # 好 - 馬上知道還有表達式接續在第一行之後
      one.two.three.
        four

    關於這兩種可供替換的寫法,可以在這裡找到討論。

  • 如果一個方法呼叫的參數擴展超過一行時,排列它們。當排列參數超過行寬限制,也可以讓第二行只縮排一層。 [link]

    # 一開始 (一行太長)
    def send_mail(source)
      Mailer.deliver(to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text)
    end
    
    # 不好 (兩倍縮排)
    def send_mail(source)
      Mailer.deliver(
          to: '[email protected]',
          from: '[email protected]',
          subject: 'Important message',
          body: source.text)
    end
    
    # 好
    def send_mail(source)
      Mailer.deliver(to: '[email protected]',
                     from: '[email protected]',
                     subject: 'Important message',
                     body: source.text)
    end
    
    # 好 (一般的縮排)
    def send_mail(source)
      Mailer.deliver(
        to: '[email protected]',
        from: '[email protected]',
        subject: 'Important message',
        body: source.text
      )
    end
  • 跨越多行來排列陣列的元素。 [link]

    # 不好 - 單層縮排
    menu_item = ['Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam',
      'Baked beans', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam']
    
    # 好
    menu_item = [
      'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam',
      'Baked beans', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam'
    ]
    
    # 好
    menu_item =
      ['Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam',
       'Baked beans', 'Spam', 'Spam', 'Spam', 'Spam', 'Spam']
  • 在語法中加入底線來改進大數值的可讀性。 [link]

    # 差勁 - 到底是有幾個零?
    num = 1000000
    
    # 良好 - 容易被人腦解讀。
    num = 1_000_000
  • 使用數值前綴時偏好使用小寫字母。 0o 用於八進位數、0x 用於十六進位數和 0b 用於二進位數。 不要使用 0d 前綴表示十進位數。 [link]

    # 不好
    num = 01234
    num = 0O1234
    num = 0X12AB
    num = 0B10101
    num = 0D1234
    num = 0d1234
    
    # 好 - 容易從前綴區分出數字
    num = 0o1234
    num = 0x12AB
    num = 0b10101
    num = 1234
  • 使用 RDoc 以及它的慣例來撰寫 API 文件。不要在註解區塊及 def 之前放一個空行。 [link]

  • 將每一行限制在最多 80 個字元。 [link]

  • 避免尾隨的空白(trailing whitesapce)。 [link]

  • 每個檔案的最後以空行做結尾。 [link]

  • 不要使用區塊註解,它們不能夠在開頭置入空白,而且無法像一般的註解一樣容易的被辨識出來。 [link]

    # 不好
    =begin
    comment line
    another comment line
    =end
    
    # 好
    # comment line
    # another comment line

語法

  • 只使用 :: 去參照常數(這包括類別與模組)與建構式(像是 Array()Nokogiri::HTML() )。不要使用 :: 作為一般的方法調用。 [link]

    # 不好
    SomeClass::some_method
    some_object::some_method
    
    # 好
    SomeClass.some_method
    some_object.some_method
    SomeModule::SomeClass::SOME_CONST
    SomeModule::SomeClass()
  • 別使用 :: 來定義類別方法。 [link]

    # 不好
    class Foo
      def self::some_method
      end
    end
    
    # 好
    class Foo
      def self.some_method
      end
    end
  • 使用 def 時,當有參數時使用括號。當方法不接受任何參數時,省略括號。 [link]

    # 不好
    def some_method()
      # 省略主體
    end
    
    # 好
    def some_method
      # 省略主體
    end
    
    # 不好
    def some_method_with_parameters param1, param2
      # 省略主體
    end
    
    # 好
    def some_method_with_parameters(param1, param2)
      # 省略主體
    end
  • 在調用方法時在參數的外圍加上括號,特別是第一個參數是以左括號開頭,如 (f((3 + 2) + 1)裡。 [link]

    # 不好
    x = Math.sin y
    # 好
    x = Math.sin(y)
    
    # 不好
    array.delete e
    # 好
    array.delete(e)
    
    # 不好
    temperance = Person.new 'Temperance', 30
    # 好
    temperance = Person.new('Temperance', 30)

    在以下的情況下盡量移除括號

    • 不帶參數的呼叫方法:

      # 不好
      Kernel.exit!()
      2.even?()
      fork()
      'test'.upcase()
      
      # 好
      Kernel.exit!
      2.even?
      fork
      'test'.upcase
    • 方法是內部領域特定語言(internal DSL)的一部分(如: Rake, Rails, RSpec):

      # 不好
      validates(:name, presence: true)
      # 好
      validates :name, presence: true
    • 在 Ruby 中有 Methods 狀態的方法:

      class Person
        # 不好
        attr_reader(:name, :age)
        # 好
        attr_reader :name, :age
      
        # body omitted
      end

    在以下的情況下可以移除括號

    • Methods that have "keyword" status in Ruby, but are not declarative:

      # 好
      puts(temperance.age)
      system('ls')
      # 也很好
      puts temperance.age
      system 'ls'
  • 將可選參數定義於參數排列的末端。 Ruby 在呼叫可選參數放在前面的方法時會出現一些無法預期的結果。 [link]

    # 不好
    def some_method(a = 1, b = 2, c, d)
      puts "#{a}, #{b}, #{c}, #{d}"
    end
    
    some_method('w', 'x') # => '1, 2, w, x'
    some_method('w', 'x', 'y') # => 'w, 2, x, y'
    some_method('w', 'x', 'y', 'z') # => 'w, x, y, z'
    
    # 好
    def some_method(c, d, a = 1, b = 2)
      puts "#{a}, #{b}, #{c}, #{d}"
    end
    
    some_method('w', 'x') # => '1, 2, w, x'
    some_method('w', 'x', 'y') # => 'y, 2, w, x'
    some_method('w', 'x', 'y', 'z') # => 'y, z, w, x'
  • 在定義變數時避免使用平行賦值(parallel assignment)。 平行賦值僅能用於呼叫方法時的回傳值、與 * 星號運算符(splat operator)共同使用、或者是用於交換變數賦值。 平行賦值的可讀性是低於分開賦值的。 [link]

    # 不好
    a, b, c, d = 'foo', 'bar', 'baz', 'foobar'
    
    # 好
    a = 'foo'
    b = 'bar'
    c = 'baz'
    d = 'foobar'
    
    # 好 - 交換變數賦值
    # 交換變數賦值是一個特殊的例子,因為他允許你交換各個已經賦予給變數的值。
    a = 'foo'
    b = 'bar'
    
    a, b = b, a
    puts a # => 'bar'
    puts b # => 'foo'
    
    # 好 - 方法回傳
    def multi_return
      [1, 2]
    end
    
    first, second = multi_return
    
    # 好 - 與 * 運算符一起使用
    first, *list = [1, 2, 3, 4] # first => 1, list => [2, 3, 4]
    
    hello_array = *'Hello' # => ["Hello"]
    
    a = *(1..3) # => [1, 2, 3]
  • 避免在平行賦值時使用不必要的尾隨底線變數,比起單純使用底線變數,為底線變數命名更好,能夠提供更多的資訊。 如果在左側的賦值有定義 * 星號變數(splat variable),且星號變數非底線變數時,尾隨底線變數是必要的。 [link]

    # 不好
    foo = 'one,two,three,four,five'
    # 不必要的賦值並沒有提供有用的資訊
    first, second, _ = foo.split(',')
    first, _, _ = foo.split(',')
    first, *_ = foo.split(',')
    
    
    # 好
    foo = 'one,two,three,four,five'
    # 必要的底線在此明確指出你需要除了最後一個元素以外的所有元素
    *beginning, _ = foo.split(',')
    *beginning, something, _ = foo.split(',')
    
    a, = foo.split(',')
    a, b, = foo.split(',')
    # 不必要的賦值給予未使用的變數,但賦值提供了我們有用的資訊
    first, _second = foo.split(',')
    first, _second, = foo.split(',')
    first, *_ending = foo.split(',')
  • 永遠不要使用 for ,除非你很清楚為什麼。大部分情況應該使用迭代器來取代。for 是由 each 所實作的(所以你加入了一層的迂迴),但出乎意料的是 — for 並沒有包含一個新的視野 (不像是 each)而在這個區塊中定義的變數將會被外部所看到。 [link]

    arr = [1, 2, 3]
    
    # 不好
    for elem in arr do
      puts elem
    end
    
    # 注意,在迴圈外一樣可以存取該元素
    elem # => 3
    
    # 好
    arr.each { |elem| puts elem }
    
    # 該元素不能在 each 的區塊之外被存取
    elem # => NameError: undefined local variable or method `elem'
  • 永遠不要在多行的 if/unless 使用 then [link]

    # 不好
    if some_condition then
      # 省略主體
    end
    
    # 好
    if some_condition
      # 省略主體
    end
  • 在多行的條件判斷區域中,永遠將條件式與 if/unless 放在同一行。 [link]

    # 不好
    if
      some_condition
      do_something
      do_something_else
    end
    
    # 好
    if some_condition
      do_something
      do_something_else
    end
  • 偏愛三元運算元 ? : 勝於 if/then/else/end 結構。它更為常見及更精準。 [link]

    # 不好
    result = if some_condition then something else something_else end
    
    # 好
    result = some_condition ? something : something_else
  • 使用一個表達式給一個三元運算元的分支。這也意味著三元運算符不要寫成巢狀式。巢狀情況使用 if/else 結構。 [link]

    # 不好
    some_condition ? (nested_condition ? nested_something : nested_something_else) : something_else
    
    # 好
    if some_condition
      nested_condition ? nested_something : nested_something_else
    else
      something_else
    end
  • 永遠不要使用 if x; ...。使用三元運算元來取代。 [link]

    # 不好
    result = if some_condition; something else something_else end
    
    # 好
    result = some_condition ? something : something_else
  • 充分利用 ifcase 會回傳結果的特性 [link]

    # 不好
    if condition
      result = x
    else
      result = y
    end
    
    # 好
    result =
      if condition
        x
      else
        y
      end
  • 一行的情況使用 when x then ... 。替代方案的語法 when x: ... 已經在 Ruby 1.9 被移除了。 [link]

  • 永遠不要使用 when x; ... 。參考前一個規則。 [link]

  • 使用 ! 而不是 not. [link]

    # 差 - 因為運算子有優先序,需要用括號。
    x = (not something)
    
    # 好
    x = !something
  • 避免使用 !![link]

    !! 會將數值轉換為布林值,但在流程控制上你不需要使用它,他只會混淆你的意圖。 如果你需要檢查 nil 值,請使用 nil? 檢查。

    # 不好
    x = 'test'
    # 令人混淆的 nil 檢查
    if !!x
      # 省略主體
    end
    
    # 好
    x = 'test'
    if x
      # 省略主體
    end
  • 禁止使用 andor。這麼做不值得。永遠使用 &&|| 取代。 [link]

    # 不好
    # 布林表達式
    if some_condition and some_other_condition
      do_something
    end
    
    # 控制流程
    document.saved? or document.save!
    
    # 好
    # 布林表達式
    if some_condition && some_other_condition
      do_something
    end
    
    # 控制流程
    document.saved? || document.save!
  • 避免多行的 ? : (三元運算元);使用 if/unless 來取代。 [link]

  • 當你有單行的主體時,偏愛 if/unless 修飾符。另一個好的方法是使用控制流程的 and/or[link]

    # 不好
    if some_condition
      do_something
    end
    
    # 好
    do_something if some_condition
    
    # 另一個好方法
    some_condition && do_something
  • 多行的複雜區塊避免使用 if/unless 修飾符。 [link]

    # 不好
    10.times do
      # 多行主體省略
    end if some_condition
    
    # 好
    if some_condition
      10.times do
        # 多行主體省略
      end
    end
  • 避免巢狀使用 if/unless/while/until 修飾符,如果 &&/|| 合適的話是更好的選擇。 [link]

    # 不好
    do_something if other_condition if some_condition
    
    # 好
    do_something if some_condition && other_condition
  • 否定條件偏愛 unless 優於 if (或是控制流程 ||)。 [link]

    # 不好
    do_something if !some_condition
    
    # 不好
    do_something if not some_condition
    
    # 好
    do_something unless some_condition
    
    # 另一個好方法
    some_condition || do_something
  • 永遠不要使用 unless 搭配 else。 將它們改寫成肯定條件。 [link]

    # 不好
    unless success?
      puts 'failure'
    else
      puts 'success'
    end
    
    # 好
    if success?
      puts 'success'
    else
      puts 'failure'
    end
  • 不要使用括號圍繞 if/unless/while 的條件式,除非這條件包含了一個賦值(見下面使用 = (一個賦值)的回傳值)。 [link]

    # 不好
    if (x > 10)
      # 省略主體
    end
    
    # 好
    if x > 10
      # 省略主體
    end

注意這項規則有個例外,條件式中的安全賦值

  • 多行的 while/until 不要使用 while/until condition do[link]

    # 不好
    while x > 5 do
      # 主體省略
    end
    
    until x > 5 do
      # 主體省略
    end
    
    # 好
    while x > 5
      # 主體省略
    end
    
    until x > 5
      # 主體省略
    end
  • 當你有一個單行的主體時,偏愛使用 while/until 修飾子。 [link]

    # 不好
    while some_condition
      do_something
    end
    
    # 好
    do_something while some_condition
  • 負面條件偏愛 until 勝於 while[link]

    # 不好
    do_something while !some_condition
    
    # 好
    do_something until some_condition
  • 當你需要使用 Kernel#loop 代替 while/until[link]

    # 不好
    while true
      do_something
    end
    
    until false
      do_something
    end
    
    # 好
    loop do
      do_something
    end
  • 迴圈後測試使用 Kernel#loop 搭配 break,而不是 begin/end/untilbegin/end/while[link]

    # 不好
    begin
      puts val
      val += 1
    end while val < 0
    
    # 好
    loop do
      puts val
      val += 1
      break unless val < 0
    end
  • 省略圍繞在隱式選項雜湊外圍的花括號。 [link]

    # 不好
    user.set({ name: 'John', age: 45, permissions: { read: true } })
    
    # 好
    user.set(name: 'John', age: 45, permissions: { read: true })
  • 方法屬於內部DSL時,同時省略外圍的花括號與圓括號。 [link]

    class Person < ActiveRecord::Base
      # 不好
      validates(:name, { presence: true, length: { within: 1..10 } })
    
      # 好
      validates :name, presence: true, length: { within: 1..10 }
    end
  • 當調用函數為區塊中的唯一操作時,使用 proc 簡單表示。 [link]

    # 不好
    names.map { |name| name.upcase }
    
    # 好
    names.map(&:upcase)
  • 單行區塊喜好 {...} 勝於 do..end。多行區塊避免使用 {...}(多行串連總是醜陋)。在 do...end 、 "控制流程" 及"方法定義",永遠使用 do...end (如 Rakefile 及某些 DSL)。串連時避免使用 do...end[link]

    names = %w[Bozhidar Steve Sarah]
    
    # 不好
    names.each do |name|
      puts name
    end
    
    # 好
    names.each { |name| puts name }
    
    # 不好
    names.select do |name|
      name.start_with?('S')
    end.map { |name| name.upcase }
    
    # 好
    names.select { |name| name.start_with?('S') }.map { |name| name.upcase }

    某些人會爭論多行串連時,使用{...}看起來還可以,但他們應該捫心自問 — 這樣程式碼真的可讀嗎?難道不能把區塊內容取出來放到絕妙的方法裡嗎?

  • 考慮使用明確的區塊參數,避免只為了傳遞參數到另一個區塊而使用區塊語法。注意區塊轉換成 Proc 所造成的效能影響。 [link]

    require 'tempfile'
    
    # 不好
    def with_tmp_dir
      Dir.mktmpdir do |tmp_dir|
        Dir.chdir(tmp_dir) { |dir| yield dir }  # block just passes arguments
      end
    end
    
    # 好
    def with_tmp_dir(&block)
      Dir.mktmpdir do |tmp_dir|
        Dir.chdir(tmp_dir, &block)
      end
    end
    
    with_tmp_dir do |dir|
      puts "dir is accessible as a parameter and pwd is set: #{dir}"
    end
  • 避免在不需要控制流程的場合時使用 return[link]

    # 不好
    def some_method(some_arr)
      return some_arr.size
    end
    
    # 好
    def some_method(some_arr)
      some_arr.size
    end
  • 避免在不需要的情況使用 self 。(只有在呼叫一個 self write 存取器時會需要用到。) [link]

    # 不好
    def ready?
      if self.last_reviewed_at > self.last_updated_at
        self.worker.update(self.content, self.options)
        self.status = :in_progress
      end
      self.status == :verified
    end
    
    # 好
    def ready?
      if last_reviewed_at > last_updated_at
        worker.update(content, options)
        status = :in_progress
      end
      status == :verified
    end
  • 避免使用帶有區域變數的 shadowing 方法,除非它們彼此相等。 [link]

    class Foo
      attr_accessor :options
    
      # ok
      def initialize(options)
        self.options = options
        # options 與 self.options 相等
      end
    
      # 不好
      def do_something(options = {})
        unless options[:when] == :later
          output(self.options[:message])
        end
      end
    
      # 好
      def do_something(params = {})
        unless params[:when] == :later
          output(options[:message])
        end
      end
    end
  • 不要在條件表達式中使用 =(賦值)回傳值,除非賦值被包在圓括號中。 這樣的做法在Ruby開發者(Rubyists)中被廣泛稱之為條件式中的安全賦值(safe assignment in condition)[link]

    # 不好 (+ 會出現警告)
    if v = array.grep(/foo/)
      do_something(v)
      ...
    end
    
    # 好 (MRI 還是會出現警告, 但 RuboCop 不會)
    if (v = array.grep(/foo/))
      do_something(v)
      ...
    end
    
    # 好
    v = array.grep(/foo/)
    if v
      do_something(v)
      ...
    end
  • 盡量使用自我賦值的簡寫寫運算元。 [link]

    # 不好
    x = x + y
    x = x * y
    x = x**y
    x = x / y
    x = x || y
    x = x && y
    
    # 好
    x += y
    x *= y
    x **= y
    x /= y
    x ||= y
    x &&= y
  • 只有在變數尚未被初始化時,使用 ||= 初始化變數。 [link]

    # 不好
    name = name ? name : 'Bozhidar'
    
    # 不好
    name = 'Bozhidar' unless name
    
    # 好 - 只有在 name 是 nil 或 false 時,設定為 Bozhidar
    name ||= 'Bozhidar'
  • 不要使用 ||= 來初始化布林變數。(想看看如果現在的值剛好是 false 時會發生什麼。) [link]

    # 不好 — 會把 enabled 設成真,即便它本來是假。
    enabled ||= true
    
    # 好
    enabled = true if enabled.nil?
  • 使用 &&= 預先處理變數是否存在。 &&= 只會更改已經存在的變數,所以能夠移除使用 if 確認變數是否存在。 [link]

    # 不好
    if something
      something = something.downcase
    end
    
    # 不好
    something = something ? something.downcase : nil
    
    # 還可以
    something = something.downcase if something
    
    # 好
    something = something && something.downcase
    
    # 更好
    something &&= something.downcase
  • 避免直接使用型態相等運算元(case equality operator) ===。它的名字暗示它絕對該被用在 case 表達式,不在此情形內則會產生一些令人困惑的程式碼。 [link]

    # 不好
    Array === something
    (1..100) === 7
    /something/ === some_string
    
    # 好
    something.is_a?(Array)
    (1..100).include?(7)
    some_string =~ /something/
  • 可以使用 == 則不要使用 eql?。實務中很少需要 eql? 所提供的嚴格比較。 [link]

    # 不好 - 對字串來說 eql? 與 == 作用一樣
    "ruby".eql? some_str
    
    # 好
    "ruby" == some_str
    1.0.eql? x # 使用 eql? 區別 Fixnum 與 Float 的 1 很合理
  • 避免使用 Perl 風格的特別變數(像是 $:$; 等等)。它們看起來非常神祕以及不鼓勵使用一行的腳本。使用函式庫 English 提供的可閱讀的別名。 [link]

    # 不好
    $:.unshift File.dirname(__FILE__)
    
    # 好
    require 'English'
    $LOAD_PATH.unshift File.dirname(__FILE__)
  • 避免在方法名與左括號之間放一個空格。 [link]

    # 不好
    f (3 + 2) + 1
    
    # 好
    f(3 + 2) + 1
  • 總是使用 -w 來執行 Ruby 直譯器,如果你忘了某個上述的規則,它就會警告你! [link]

  • 不要使用巢狀定義方法,使用 lambda 取代。 巢狀方法定義會在同一個範圍(例如 class)內實作出與外面相同的方法,而且, "巢狀方法" 在每一次呼叫包含定義的方法時都會重新定義。 [link]

    # 不好
    def foo(x)
      def bar(y)
        # 主體省略
      end
    
      bar(x)
    end
    
    # 好 - 與上一個例子相同,但是每次呼叫 foo 並不會重新定義 bar
    def bar(y)
      # 主體省略
    end
    
    def foo(x)
      bar(x)
    end
    
    # 也很好
    def foo(x)
      bar = ->(y) { ... }
      bar.call(x)
    end
  • 對單行區塊主體使用新的 lambda 字面語法,多行區塊使用 lambda 方法。 [link]

    # 不好
    lambda = lambda { |a, b| a + b }
    lambda.call(1, 2)
    
    # 正確,但使用非常不方便
    l = ->(a, b) do
      tmp = a * 7
      tmp * b / 50
    end
    
    # 好
    l = ->(a, b) { a + b }
    l.call(1, 2)
    
    l = lambda do |a, b|
      tmp = a * 7
      tmp * b / 50
    end
  • 在定義 stabby lambda (使用 -> 箭頭符號)時別省略參數外的括號。 [link]

    # 不好
    l = ->x, y { something(x, y) }
    
    # 好
    l = ->(x, y) { something(x, y) }
  • 在定義 stabby lambda (使用 -> 箭頭符號)時,如果沒有參數的話,省略括號。 [link]

    # 不好
    l = ->() { something }
    
    # 好
    l = -> { something }
  • 偏愛 proc 勝過 Proc.new[link]

    # 不好
    p = Proc.new { |n| puts n }
    
    # 好
    p = proc { |n| puts n }
  • 對 lambdas 與 procs ,偏愛 proc.call() 勝過 proc[] 或是 proc.()[link]

    # 不好 - 與列舉(Enumeration)的存取相似
    l = ->(v) { puts v }
    l[1]
    
    # 還是不好 - 不常見的語法
    l = ->(v) { puts v }
    l.(1)
    
    # 好
    l = ->(v) { puts v }
    l.call(1)
  • 將沒有使用的區塊參數與區域變數加上前綴 _ ,也可以只使用 _ (雖然少了一點描述)。 Ruby 直譯器與工具像是 RuboCop 可以解析這個慣例,不會發出尚未使用變數的警告。 [link]

    # 不好
    result = hash.map { |k, v| v + 1 }
    
    def something(x)
      unused_var, used_var = something_else(x)
      # ...
    end
    
    # 好
    result = hash.map { |_k, v| v + 1 }
    
    def something(x)
      _unused_var, used_var = something_else(x)
      # ...
    end
    
    # 好
    result = hash.map { |_, v| v + 1 }
    
    def something(x)
      _, used_var = something_else(x)
      # ...
    end
  • 使用 $stdout/$stderr/$stdin 而不是 STDOUT/STDERR/STDINSTDOUT/STDERR/STDIN 是常數, 在Ruby中的確可以將常數重新賦值(也許是想重新導向到某個串流) ,但直譯器會發出警告。 [link]

  • 使用 warn 而不是 $stderr.puts。 除了比較清晰簡潔外, warn 允許停用警告(透過 -W0 設定警告級別為 0 )。 [link]

  • 偏愛使用 sprintf 與它的別名 format,而不是使用相當晦澀的 String#% 方法。 [link]

    # 不好
    '%d %d' % [20, 10]
    # => '20 10'
    
    # 好
    sprintf('%d %d', 20, 10)
    # => '20 10'
    
    # 好
    sprintf('%{first} %{second}', first: 20, second: 10)
    # => '20 10'
    
    format('%d %d', 20, 10)
    # => '20 10'
    
    # 好
    format('%{first} %{second}', first: 20, second: 10)
    # => '20 10'
  • 使用插值於格式化字串時,偏好使用 %<name>s 勝於 %{name},因為他帶有此數值類型的資訊。 [link]

    # 不好
    format('Hello, %{name}', name: 'John')
    
    # 好
    format('Hello, %<name>s', name: 'John')
  • 偏愛使用 Array#join 而不是相當晦澀帶有字串參數的 Array#*[link]

    # 不好
    %w(one two three) * ', '
    # => 'one, two, three'
    
    # 好
    %w(one two three).join(', ')
    # => 'one, two, three'
  • 當想要使用作為陣列用途的變數,但並不確定它是否為陣列,利用 [*var] 或是 Array() 而不是直接進行 Array 檢查。 [link]

    # 不好
    paths = [paths] unless paths.is_a? Array
    paths.each { |path| do_something(path) }
    
    # 好
    [*paths].each { |path| do_something(path) }
    
    # 好 (更加可讀)
    Array(paths).each { |path| do_something(path) }
  • 當可以使用範圍或是 Comparable#between? 就別使用複雜的邏輯比較。 [link]

    # 不好
    do_something if x >= 1000 && x <= 2000
    
    # 好
    do_something if (1000..2000).include?(x)
    
    # 好
    do_something if x.between?(1000, 2000)
  • 偏愛使用述語方法(predicate methods)與 == 做出明確的區別。可以接受數字比較。 [link]

    # 不好
    if x % 2 == 0
    end
    
    if x % 2 == 1
    end
    
    if x == nil
    end
    
    # 好
    if x.even?
    end
    
    if x.odd?
    end
    
    if x.nil?
    end
    
    if x.zero?
    end
    
    if x == 0
    end
  • 不使用明確的非- nil 檢查,除非是處理布林值。 [link]

    # 不好
    do_something if !something.nil?
    do_something if something != nil
    
    # 好
    do_something if something
    
    # 好 - 處理布林值
    def value_set?
      !@some_boolean.nil?
    end
  • 避免使用 BEGIN 區塊。 [link]

  • 不要使用 END 區塊。使用 Kernel#at_exit[link]

    # 不好
    END { puts 'Goodbye!' }
    
    # 好
    at_exit { puts 'Goodbye!' }
  • 避免使用範圍運算元(flip-flops)。 [link]

  • 避免在流程控制中使用巢狀的條件。 [link]

    當你能斷定無效的資料時,則偏好使用守衛語句(guard clause)。 守衛語句是在函式最上方的條件句,在必要的時候能夠盡快地脫離方法。

    # 不好
    def compute_thing(thing)
      if thing[:foo]
        update_with_bar(thing)
        if thing[:foo][:bar]
          partial_compute(thing)
        else
          re_compute(thing)
        end
      end
    end
    
    # 好
    def compute_thing(thing)
      return unless thing[:foo]
      update_with_bar(thing[:foo])
      return re_compute(thing) unless thing[:foo][:bar]
      partial_compute(thing)
    end

    偏愛在迴圈中使用 next 而不是條件區塊。

    # 不好
    [0, 1, 2, 3].each do |item|
      if item > 1
        puts item
      end
    end
    
    # 好
    [0, 1, 2, 3].each do |item|
      next unless item > 1
      puts item
    end
  • 偏好 mapcollect 好, finddetect 好, selectfind_all 好, reduceinject 好, sizelength 好。這並非是硬性的需求;如果使用別名能增強可讀性,那就用吧。 押韻方法(rhyming methods)沿襲自 Smalltalk ,這在其他程式語言中並不常見。鼓勵使用 select 大於 find_all ,因為能夠很好的與 reject 放在一起,而且它的名字能夠解釋自己的作用。 [link]

  • 不要使用 count 作為 size 的替代方案。除了 Enumerable 物件之外還有 Array ,都會遍歷整個集合來取得它的大小。 [link]

    # 不好
    some_hash.count
    
    # 好
    some_hash.size
  • 使用 flat_map 而不是 map + flatten。這不適用於深度大於兩層的陣列,舉例來說,如果 users.first.songs == ['a', ['b','c']] ,則使用 map + flatten 而不是 flat_mapflat_map 扁平化第一層的陣列,然而 flatten 所有情形都會進行扁平化。 [link]

    # 不好
    all_songs = users.map(&:songs).flatten.uniq
    
    # 好
    all_songs = users.flat_map(&:songs).uniq
  • 使用 reverse_each 而不是 reverse.eachreverse_each 不會製造一個新的陣列作為分配用途,這樣很好。 [link]

    # 不好
    array.reverse.each { ... }
    
    # 好
    array.reverse_each { ... }

命名

程式設計的真正難題是替事物命名及無效的快取。
-- Phil Karlton

  • 識別字用英語命名。 [link]

    # 不好 - 識別字使用非 ASCII 字母
    заплата = 1_000
    
    # 不好 - 識別字使用保加利亞語英語拼音(相較前例使用斯拉夫拼音)
    zaplata = 1_000
    
    # 好
    salary = 1_000
  • 符號、方法與變數使用蛇底式小寫(snake_case)。 [link]

    # 不好
    :'some symbol'
    :SomeSymbol
    :someSymbol
    
    someVar = 5
    
    def someMethod
      ...
    end
    
    def SomeMethod
     ...
    end
    
    # 好
    :some_symbol
    
    some_var = 5
    var10    = 10
    
    def some_method
      ...
    end
  • 在命名符號、方法或變數時,不要分開字母跟數字。 [link]

    # 不好
    :some_sym_1
    
    some_var_1 = 1
    
    def some_method_1
      # 一些程式碼
    end
    
    # 好
    :some_sym1
    
    some_var1 = 1
    
    def some_method1
      # 一些程式碼
    end
  • 類別與模組使用駝峰式大小寫(CamelCase)。(保留像是 HTTP、RFC、XML 這種縮寫為大寫) [link]

    # 不好
    class Someclass
      # 一些程式碼
    end
    
    class Some_Class
      # 一些程式碼
    end
    
    class SomeXml
      # 一些程式碼
    end
    
    class XmlSomething
      # 一些程式碼
    end
    
    # 好
    class SomeClass
      # 一些程式碼
    end
    
    class SomeXML
      # 一些程式碼
    end
    
    class XMLSomething
      # 一些程式碼
    end
  • 檔案命名使用蛇底式小寫(snake_case),例如: hello_world.rb[link]

  • 資料夾命名亦使用蛇底式小寫(snake_case),例如: lib/hello_world/hello_world.rb[link]

  • 盡量讓每一個檔案內只有單一類別或模組。使用該類別或模組的名稱來替該檔案命名,但將名稱從駝峰式大小寫(CamelCase)置換成蛇底式小寫(snake_case)。 [link]

  • 其他常數使用尖叫蛇底式大寫(SCREAMING_SNAKE_CASE)。 [link]

    # 不好
    SomeConst = 5
    
    # 好
    SOME_CONST = 5
  • 判斷式方法的名字(回傳布林值的方法)應以問號結尾。(即 Array#empty? )。非回傳布林值的方法,不應以問號結尾。 [link]

  • 避免在使用動詞命名的判斷式加上前綴如 isdoes 或是 can,這些前綴與 Ruby 核心函式庫內的判斷式如 empty?include? 相較之下顯得多餘且邏輯不一致。 [link]

    # 不好
    class Person
      def is_tall?
        true
      end
    
      def can_play_basketball?
        false
      end
    
      def does_like_candy?
        true
      end
    end
    
    # 好
    class Person
      def tall?
        true
      end
    
      def basketball_player?
        false
      end
    
      def likes_candy?
        true
      end
    end
  • 有潛在“危險性”的方法,若此 危險 方法有安全版本存在時,應以安全版本名加上驚嘆號結尾(即:改動 self 或參數、 exit! 等等方法)。 [link]

    # 不好 - 沒有對應的安全方法
    class Person
      def update!
      end
    end
    
    # 好
    class Person
      def update
      end
    end
    
    # 好
    class Person
      def update!
      end
    
      def update
      end
    end
  • 如果可能的話,從危險方法(bang)的角度來定義對應的安全方法(non-bang)。 [link]

    class Array
      def flatten_once!
        res = []
    
        each do |e|
          [*e].each { |f| res << f }
        end
    
        replace(res)
      end
    
      def flatten_once
        dup.flatten_once!
      end
    end
  • 在定義二元操作符時,把參數命名為 other<<[] 是這條規則的例外,因為它們的語義不同)。 [link]

    def +(other)
      # 省略主體
    end

註解

良好的程式碼是最佳的文件。當你要加一個註解時,捫心自問,
"如何改善程式碼讓它不需要註解?" 改善程式碼然後記錄下來使它更簡潔。
-- Steve McConnell

  • 撰寫自我記錄的程式碼並忽略之後的小節。我是認真的! [link]

  • 用英文寫註解。 [link]

  • 在註解的 # 與註解文字之間使用一個空格。 [link]

  • 比一個單字長的註解要大寫及使用標點符號。句號後使用一個空格[link]

  • 避免多餘的註解 [link]

    # 不好
    counter += 1 # 把計數器加一
  • 保持現有的註解是最新的。過時的註解比沒有註解還差。 [link]

好的程式碼就像是好的笑話 -- 它不需要解釋
-- Russ Olsen

  • 避免替爛程式碼寫註解。重構程式碼讓它們看起來一目了然。(要嘛就做,要嘛不做 ― 不要只是試試看。-- Yoda) [link]

註釋

  • 註釋應該直接寫在相關程式碼那行之後。 [link]

  • 註釋關鍵字後方伴隨著一個冒號及空白,接著一個描述問題的記錄。 [link]

  • 如果需要用多行來描述問題,之後的行要放在 # 號後面並縮排兩個空白。 [link]

    def bar
      # FIXME: 這在 v3.2.1 版本之後會異常當掉,或許與
      #   BarBazUtil 的版本更新有關
      baz(:quux)
    end
  • 在問題是顯而易見的情況下,任何的文件會是多餘的,註釋應該要留在可能有問題的那行。這個用法是例外而不是規則。 [link]

    def bar
      sleep 100 # OPTIMIZE
    end
  • 使用 TODO 來標記之後應被加入的未實現功能或特色。 [link]

  • 使用 FIXME 來標記一個需要修復的程式碼。 [link]

  • 使用 OPTIMIZE 來標記可能影響效能的緩慢或效率低落的程式碼。 [link]

  • 使用 HACK 來標記代碼異味,其中包含了可疑的編碼實踐以及應該需要重構。 [link]

  • 使用 REVIEW 來標記任何需要審視及確認正常動作的地方。舉例來說:REVIEW: 我們確定用戶現在是這麼做的嗎? [link]

  • 如果你覺得適當的話,使用其他你習慣的註釋關鍵字,但記得把它們記錄在專案的 README 或類似的地方。 [link]

魔法註解

  • 在代碼或是文件的最上方加入魔法註解(除了 shebangs ,我們等等會討論到他)。 [link]

    # 好
    # frozen_string_literal: true
    
    # 有關 Person 的一些文件
    class Person
    end
    
    # 不好
    # 有關 Person 的一些文件
    
    # frozen_string_literal: true
    class Person
    end
  • 當魔法註解與 shebangs 同時出現的話,將前者至於後者之後。 [link]

    # 好
    #!/usr/bin/env ruby
    # frozen_string_literal: true
    
    App.parse(ARGV)
    
    # 不好
    # frozen_string_literal: true
    #!/usr/bin/env ruby
    
    App.parse(ARGV)
  • 你如果需要多個的話,使用一行一個魔法註解。 [link]

    # 好
    # frozen_string_literal: true
    # encoding: ascii-8bit
    
    # 不好
    # -*- frozen_string_literal: true; encoding: ascii-8bit -*-
  • 使用一個空行分隔魔法註解與程式碼或是文件。 [link]

    # 好
    # frozen_string_literal: true
    
    # 有關 Person 的一些文件
    class Person
      # Some code
    end
    
    # 不好
    # frozen_string_literal: true
    # 有關 Person 的一些文件
    class Person
      # Some code
    end

類別與模組

  • 在類別定義裡使用一致的結構。 [link]

    class Person
      # 首先是 extend 與 include
      extend SomeModule
      include AnotherModule
    
      # 內部類別
      CustomError = Class.new(StandardError)
    
      # 接著是常數
      SOME_CONSTANT = 20
    
      # 接下來是屬性巨集
      attr_reader :name
    
      # 跟著是其它的巨集(如果有的話)
      validates :name
    
      # 公開的類別方法接在下一行
      def self.some_method
      end
    
      # 初始化方法置於類別方法與其他實例方法之中
      def initialize
      end
    
      # 跟著是公開的實體方法
      def some_method
      end
    
      # 受保護及私有的方法,一起放在接近結尾的地方
      protected
    
      def some_protected_method
      end
    
      private
    
      def some_private_method
      end
    end
  • 引入多個 mixin 時分開聲明。 [link]

    # 不好
    class Person
      include Foo, Bar
    end
    
    # 好
    class Person
      # 把多個 mixin 分開聲明
      include Foo
      include Bar
    end
  • 別在同一個類別中巢狀多個類別,試著在各別的類別檔案中分開包裝類別。 [link]

    # 不好
    
    # foo.rb
    class Foo
      class Bar
        # 裡面有 30 個方法
      end
    
      class Car
        # 裡面有 20 個方法
      end
    
      # 裡面有 30 個方法
    end
    
    # 好
    
    # foo.rb
    class Foo
      # 裡面有 30 個方法
    end
    
    # foo/bar.rb
    class Foo
      class Bar
        # 裡面有 30 個方法
      end
    end
    
    # foo/car.rb
    class Foo
      class Car
        # 裡面有 20 個方法
      end
    end
  • 使用明確的巢狀定義(和重啟)命名類別和模組,因為 Ruby 的詞法作用域(lexical scoping),使用範圍解析運算符(::)時,根據巢狀模組定義的方式,可能導致意料之外的常數查找。 [link]

    module Utilities
      class Queue
      end
    end
    
    # 不好
    class Utilities::Store
      Module.nesting # => [Utilities::Store]
    
      def initialize
        # 參考到最高層級 ::Queue 類別,因為 Utilities 並不在當前的巢狀鏈中
        @queue = Queue.new
      end
    end
    
    # 好
    module Utilities
      class WaitingList
        Module.nesting # => [Utilities::WaitingList, Utilities]
    
        def initialize
          @queue = Queue.new # 參考 Utilities::Queue
        end
      end
    end
  • 偏好模組,勝過只有類別方法的類。類別應該只在產生實例是合理的時候使用。 [link]

    # 差
    class SomeClass
      def self.some_method
        # 主體省略
      end
    
      def self.some_other_method
      end
    end
    
    # 好
    module SomeClass
      module_function
    
      def some_method
        # 主體省略
      end
    
      def some_other_method
      end
    end
  • 當你想將模組的實例方法變成類別方法時,偏愛使用 module_function 勝過 extend self[link]

    # 差
    module Utilities
      extend self
    
      def parse_something(string)
        # do stuff here
      end
    
      def other_utility_method(number, string)
        # do some more stuff
      end
    end
    
    # 好
    module Utilities
      module_function
    
      def parse_something(string)
        # do stuff here
      end
    
      def other_utility_method(number, string)
        # do some more stuff
      end
    end
  • 當設計類別階層時,確認它們符合 Liskov 代換原則[link]

  • 盡可能讓你的類別越堅固越好。 [link]

  • 永遠替類別提供一個適當的 to_s 方法來表示領域模型(domain model)。 [link]

    class Person
      attr_reader :first_name, :last_name
    
      def initialize(first_name, last_name)
        @first_name = first_name
        @last_name = last_name
      end
    
      def to_s
        "#{@first_name} #{@last_name}"
      end
    end
  • 使用 attr 這類函數來定義瑣碎的 accessor 或 mutators。 [link]

    # 不好
    class Person
      def initialize(first_name, last_name)
        @first_name = first_name
        @last_name = last_name
      end
    
      def first_name
        @first_name
      end
    
      def last_name
        @last_name
      end
    end
    
    # 好
    class Person
      attr_reader :first_name, :last_name
    
      def initialize(first_name, last_name)
        @first_name = first_name
        @last_name = last_name
      end
    end
  • 使用存取器(accessors)和修改器(mutators)時,避免在方法名稱前面加上 get_set_ 的前綴。在 Ruby 中,慣例上使用屬性名稱當作存取器(讀)和使用 attr_name= 當修改器(寫)。 [link]

    # 不好
    class Person
      def get_name
        "#{@first_name} #{@last_name}"
      end
    
      def set_name(name)
        @first_name, @last_name = name.split(' ')
      end
    end
    
    # 好
    class Person
      def name
        "#{@first_name} #{@last_name}"
      end
    
      def name=(name)
        @first_name, @last_name = name.split(' ')
      end
    end
  • 避免使用 attr。 使用 attr_readerattr_accessor[link]

    # 不好 - 產生一個屬性存取器 (Ruby 1.9 時已移除)
    attr :something, true
    attr :one, :two, :three # 與 attr_reader 作用相同
    
    # 好
    attr_accessor :something
    attr_reader :one, :two, :three
  • 考慮使用 Struct.new,它替你定義了那些瑣碎的存取器(accessors),建構式(constructor)以及比較運算元(comparison operators)。 [link]

    # 好
    class Person
      attr_reader :first_name, :last_name
    
      def initialize(first_name, last_name)
        @first_name = first_name
        @last_name = last_name
      end
    end
    
    # 較佳
    Person = Struct.new(:first_name, :last_name) do
    end
  • 別使用 Struct.new 擴展一個實例初始化,如果檔案引入太多次,在帶入多餘的類別來擴展可能也會同時來入了奇怪的錯誤。 [link]

    # 不好
    class Person < Struct.new(:first_name, :last_name)
    end
    
    # 好
    Person = Struct.new(:first_name, :last_name)
  • 考慮加入工廠方法來提供額外合理的方式,來創造一個特定類別的實體。 [link]

    class Person
      def self.create(options_hash)
        # 主體省略
      end
    end
  • 偏好鴨子類型勝於繼承。 [link]

    # 不好
    class Animal
      # 抽象方法
      def speak
      end
    end
    
    # 繼承高層次的類別 (superclass)
    class Duck < Animal
      def speak
        puts 'Quack! Quack'
      end
    end
    
    # 繼承高層次的類別 (superclass)
    class Dog < Animal
      def speak
        puts 'Bau! Bau!'
      end
    end
    
    # 好
    class Duck
      def speak
        puts 'Quack! Quack'
      end
    end
    
    class Dog
      def speak
        puts 'Bau! Bau!'
      end
    end
  • 由於繼承中 "討厭的" 行為,避免使用類別變數 (@@)。 [link]

    class Parent
      @@class_var = 'parent'
    
      def self.print_class_var
        puts @@class_var
      end
    end
    
    class Child < Parent
      @@class_var = 'child'
    end
    
    Parent.print_class_var # => will print "child"

    如同你所看到的,在類別階級中的所有類別其實都共享一個類別變數。應該通常偏好使用實體變數而不是類別變數。

  • 依據方法的目的用途指定適當的可視層級 (private ,protected )。別把所有方法都設為 public (方法的預設值)。我們現在是在寫 Ruby ,不是 Python[link]

  • public, protected, private 和被應用的方法定義保持一致的縮排。在上下各留一行,來強調在這些方法的特性(公有的、受保護的、私有的)。 [link]

    class SomeClass
      def public_method
        # ...
      end
    
      private
    
      def private_method
        # ...
      end
    
      def another_private_method
        # ...
      end
    end
  • 使用 def self.method 來定義 singleton 方法。由於類別名稱不重複的關係,這使得代碼更容易重構。 [link]

    class TestClass
      # 不好
      def TestClass.some_method
        # 省略主體
      end
    
      # 好
      def self.some_other_method
        # 省略主體
      end
    
      # 也有可能且當你要定義多個
      # singleton時的便利方法
      class << self
        def first_method
          # 省略主體
        end
    
        def second_method_etc
          # 省略主體
        end
      end
    end
  • 當要幫在同一個類別作用域(lexical class scope)的方法使用別名時, 比起使用 self 的解法,偏好使用 alias , 它更加清楚的告訴使用者,除非明確聲明,否則你的別名不會被任何子類別修改,運行時亦同。 [link]

    class Westerner
      def first_name
        @names.first
      end
    
      alias given_name first_name
    end

    由於 alias 如同 def,是個關鍵字,所以偏好無型態符號的參數(bareword arguments)勝過 符號或是字串,換個說法,使用 alias foo bar 而不是 alias :foo :bar

    同時注意 Ruby 如何處理別名和繼承:當別名被定義時,別名的參照已經決定了,而非動態發出。

    class Fugitive < Westerner
      def first_name
        'Nobody'
      end
    end

    在這個例子裡 Fugitive#given_name 仍會呼叫 Westerner#first_name 方法, 而非 Fugitive#first_name. 想要複寫 Fugitive#given_name 的行為,你必須重新定義初始的類別。

    class Fugitive < Westerner
      def first_name
        'Nobody'
      end
    
      alias given_name first_name
    end
  • 當別名使用在模組、類別或是運行中單例類別的方法時,使用 alias_method , 在這些情境下,使用作用域中的 alias 將會使運行無法預期。 [link]

    module Mononymous
      def self.included(other)
        other.class_eval { alias_method :full_name, :given_name }
      end
    end
    
    class Sting < Westerner
      include Mononymous
    end
  • 當一個類別(或是模組)方法呼叫其他類似的方法時,隱藏開頭的 self 或是隱藏在 . 之後自己的名字。 這在服務類別(service classes)或是其他把類別當作函式使用的概念中常見,這樣的習慣會減少這種類別出現重複樣板。 [link]

    class TestClass
      # 不好 -- 類別重新命名或是方法變更時需要花更多功夫
      def self.call(param1, param2)
        TestClass.new(param1).call(param2)
      end
    
      # 不好 -- 過於詳細
      def self.call(param1, param2)
        self.new(param1).call(param2)
      end
    
      # 好
      def self.call(param1, param2)
        new(param1).call(param2)
      end
    
      # 其他方法...
    end

異常

  • 對於異常建議使用 raise 勝過 fail[link]

    # bad
    fail SomeException, 'message'
    
    # good
    raise SomeException, 'message'
  • 當使用兩個參數的 raise 時,別明確指出 RuntimeError[link]

    # 不好
    raise RuntimeError, 'message'
    
    # 好 - 單個參數預設為 RuntimeError
    raise 'message'
  • 偏好提供給 raise 異常類別和訊息共兩個分開的參數,勝於直接給異常實例。 [link]

    # 不好
    raise SomeException.new('message')
    # 這邊是沒有辦法使用 `raise SomeException.new('message'), backtrace` 的.
    
    # 好
    raise SomeException, 'message'
    # 與 `raise SomeException, 'message', backtrace` 一致.
  • 永遠不要從 ensure 區塊返回。如果你顯式地從 ensure 區塊中的一個方法返回,那麼這方法會如同沒有異常般的返回。實際上,異常會被默默地丟掉。 [link]

    def foo
      begin
        fail
      ensure
        return 'very bad idea'
      end
    end
  • 盡可能使用隱式的 begin 區塊。 [link]

    # 不好
    def foo
      begin
        # main logic goes here
      rescue
        # failure handling goes here
      end
    end
    
    # 好
    def foo
      # main logic goes here
    rescue
      # failure handling goes here
    end
  • 透過 contingency 方法 (一個由 Avdi Grimm 創造的詞)來減少 begin 區塊的使用。 [link]

    # 不好
    begin
      something_that_might_fail
    rescue IOError
      # handle IOError
    end
    
    begin
      something_else_that_might_fail
    rescue IOError
      # handle IOError
    end
    
    # 好
    def with_io_error_handling
       yield
    rescue
      # handle IOError
    end
    
    with_io_error_handling { something_that_might_fail }
    
    with_io_error_handling { something_else_that_might_fail }
  • 不要封鎖異常。 [link]

    begin
      # 這裡發生了一個異常
    rescue SomeError
      # 救援子句完全沒有做事
    end
    
    # bad
    do_something rescue nil
  • 避免以修飾符的形式使用 rescue[link]

    # 不好 - 這會捕捉 StandardError 類別和其所有子孫類別
    read_file rescue handle_error($!)
    
    # 好 - 這只會捕捉 Errno::ENOENT 類別和其所有子孫類別
    def foo
      read_file
    rescue Errno::ENOENT => ex
      handle_error(ex)
    end
  • 不要為了控制流程而使用異常。 [link]

    # 不好
    begin
      n / d
    rescue ZeroDivisionError
      puts 'Cannot divide by 0!'
    end
    
    # 好
    if d.zero?
      puts 'Cannot divide by 0!'
    else
      n / d
    end
  • 避免救援 Exception 類別。這會困住信號與對 exit 的呼叫,導致你需要 kill -9 進程。 [link]

    # 不好
    begin
      # 呼叫 exit 及 kill 信號會被捕捉(除了 kill -9)
      exit
    rescue Exception
      puts "you didn't really want to exit, right?"
      # 異常處理
    end
    
    # 好
    begin
      # 從 StandardError 中救援一個救援子句,
      # 不是許多程式設計師所假定的異常。
    rescue => e
      # 異常處理
    end
    
    # 也很好
    begin
      # 這裡發生一個異常
    
    rescue StandardError => e
      # 異常處理
    end
  • 把較具體的異常放在救援串連的較上層,不然它們永遠不會被救援。 [link]

    # 不好
    begin
      # 一些程式碼
    rescue Exception => e
      # 一些處理
    rescue StandardError => e
      # 一些處理
    end
    
    # 好
    begin
      # 一些程式碼
    rescue StandardError => e
      # 一些處理
    rescue Exception => e
      # 一些處理
    end
  • 在 ensure 區塊中釋放你程式的外部資源。 [link]

    f = File.open('testfile')
    begin
      # .. 處理
    rescue
      # .. 錯誤處理
    ensure
      f.close unless f.nil?
    end
  • 盡量使用可自動清理資源的取得方法。 [link]

    # 不好 - 你需要明確關閉檔案描述符(file descriptor)
    f = File.open('testfile')
    # 檔案裡的一些動作
    f.close
    
    # 好 - 檔案描述符會自行關閉
    File.open('testfile') do |f|
      # 檔案裡的一些動作
    end
  • 偏愛使用標準函式庫的異常處理勝於導入新的異常類別。 [link]

集合

  • 偏好陣列及雜湊的字面表示法(除非你需要給建構子傳入參數)。 [link]

    # 不好
    arr = Array.new
    hash = Hash.new
    
    # 好
    arr = []
    arr = Array.new(10)
    hash = {}
    hash = Hash.new(0)
  • 當陣列由兩個以上的字串組成,而且字串不包含空格及特別字元時,偏好使用 %w 創建陣列。 [link]

    # 不好
    STATES = ['draft', 'open', 'closed']
    
    # 好
    STATES = %w(draft open closed)
  • 當陣列由兩個以上的 symbol 組成,而且你不需要維護 Ruby 1.9 兼容性時,偏好使用 %i 創建陣列。 [link]

    # bad
    STATES = [:draft, :open, :closed]
    
    # good
    STATES = %i(draft open closed)
  • 避免在 ArrayHash 的最後一個項目後使用逗號,特別是他們不是分行表示時。 [link]

    # 不好 - 易於移動、增加、移除項目,但仍不是好選擇
    VALUES = [
               1001,
               2020,
               3333,
             ]
    
    # 不好
    VALUES = [1001, 2020, 3333, ]
    
    # 好
    VALUES = [1001, 2020, 3333]
  • 避免在陣列中創造巨大的間隔。 [link]

    arr = []
    arr[100] = 1 # 現在你有一個很多 nil 的陣列
  • 當需要取得陣列第一個或是最後一個元素時,偏好 使用 firstlast 勝過 [0][-1][link]

  • 當處理獨一無二的元素時,使用 Set 來替代 ArraySet 實現了不重複的無序數值集合。Set 是陣列直觀的內部操作功能與雜湊的快速存取的混合體。 [link]

  • 偏好用符號取代字串作為雜湊的鍵。 [link]

    # 不好
    hash = { 'one' => 1, 'two' => 2, 'three' => 3 }
    
    # 好
    hash = { one: 1, two: 2, three: 3 }
  • 避免使用可變的物件作為鍵值。 [link]

  • 當你的 hash 鍵為符號時,使用雜湊的字面語法。 [link]

    # 不好
    hash = { :one => 1, :two => 2, :three => 3 }
    
    # 好
    hash = { one: 1, two: 2, three: 3 }
  • 別在同一個雜湊表示中混用 Ruby 1.9 雜湊表示式和火箭表示式(=>)。 當你的鍵值不是符號時,他就是雜湊火箭表示式 [link]

    # 不好
    { a: 1, 'b' => 2 }
    
    # 好
    { :a => 1, 'b' => 2 }
  • 使用 Hash#key? 取代 Hash#has_key? 和 使用 Hash#value? 取代 Hash#has_value?[link]

    # 不好
    hash.has_key?(:test)
    hash.has_value?(value)
    
    # 好
    hash.key?(:test)
    hash.value?(value)
  • 使用 Hash#each_key 取代 Hash#keys.each 和使用 Hash#each_value 取代 Hash#values.each[link]

    # 不好
    hash.keys.each { |k| p k }
    hash.values.each { |v| p v }
    hash.each { |k, _v| p k }
    hash.each { |_k, v| p v }
    
    # 好
    hash.each_key { |k| p k }
    hash.each_value { |v| p v }
  • 在處理需要出現的雜湊鍵時,使用 fetch[link]

    heroes = { 蝙蝠俠: 'Bruce Wayne', 超人: 'Clark Kent' }
    # 差勁 - 如果我們打錯字的話,我們就無法找到對的英雄了
    heroes[:蝙蝠俠] # => "Bruce Wayne"
    heroes[:超女] # => nil
    
    # 棒 - fetch 會拋出一個 KeyError 來體現這個問題
    heroes.fetch(:supermann)
  • 透過 Hash#fetch 來引入雜湊的值,而非使用邏輯符。 [link]

    batman = { name: 'Bruce Wayne', is_evil: false }
    
    # 不好 - 如果只使用 || 運算符與假值,我們不會得到預期的結果
    batman[:is_evil] || true # => true
    
    # 好 - fetch 與假值正常運作中
    batman.fetch(:is_evil, true) # => false
  • 如果程式碼需要求值,可能會導致副作用或是成本過高時,偏好使用程式區塊(block)取代 Hash#fetch 裡的預設值。 [link]

    batman = { name: 'Bruce Wayne' }
    
    # 不好 - 如果我們使用預設值,便急於對他求值
    # 發生多次的話它將會拖慢程式
    batman.fetch(:powers, obtain_batman_powers) # obtain_batman_powers 是個非常重量級的呼叫
    
    # 好 - 程式區塊為惰性求值,他僅會在 KeyError 異常時作動
    batman.fetch(:powers) { obtain_batman_powers }
  • 當你需要從雜湊中連續檢索多個值時,使用Hash#values_at[link]

    # 不好
    email = data['email']
    username = data['nickname']
    
    # 好
    email, username = data.values_at('email', 'nickname')
  • 相信這個事實吧,Ruby 1.9 的雜湊是有序的。 [link]

  • 在遍歷一個集合時,不要改動它。 [link]

  • 當要取用集合的元素時,如有提供的話盡量使用讀取器的替代方法,避免直接使用 [n] 取用。 這可以讓你避免從 nil 上呼叫 [][link]

    # 不好
    Regexp.last_match[1]
    
    # 好
    Regexp.last_match(1)
  • 要為一個集合提供存取器時,提供一個替代方法在存取時幫使用者檢查是否存取到 nil[link]

    # 不好
    def awesome_things
      @awesome_things
    end
    
    # 好
    def awesome_things(index = nil)
      if index && @awesome_things
        @awesome_things[index]
      else
        @awesome_things
      end
    end

數字

  • 使用 Integer 來檢查整數的類型,因為 Fixnum 的定義取決於平台,在 32 位元與 64 位元的機器上會回傳不同的結果。 [link]

    timestamp = Time.now.to_i
    
    # 不好
    timestamp.is_a? Fixnum
    timestamp.is_a? Bignum
    
    # 好
    timestamp.is_a? Integer
    • 在生成隨機數時偏好使用範圍(ranges),而不是帶偏移量的整數,因為他明確的展現你的意圖,想像一下你真的在骰一個骰子。 [link]

      # 不好
      rand(6) + 1
      
      # 好
      rand(1..6)

字串

  • 偏好字串插值 (interpolation),而不是字串串接 (concatenation)。 [link]

    # 不好
    email_with_name = user.name + ' <' + user.email + '>'
    
    # 好
    email_with_name = "#{user.name} <#{user.email}>"
    
    # 好
    email_with_name = format('%s <%s>', user.name, user.email)
  • 選擇一個統一的字串風格吧!在 Ruby 社群中有兩種普遍的方法,分別是預設使用單引號(選項一)跟預設使用雙引號(選項二)。 [link]

    • (選項一) 當你不需要插入特殊符號如 \t, \n, ', 等等時,偏好單引號的字串。

      # 不好
      name = "Bozhidar"
      
      name = 'De\'Andre'
      
      # 好
      name = 'Bozhidar'
      
      name = "De'Andre"
    • (選項二) 偏好使用雙引號,除非你的字串內包含 " 或是你必須要避免跳脫字元。

      # 不好
      name = 'Bozhidar'
      
      sarcasm = "I \"like\" it."
      
      # 好
      name = "Bozhidar"
      
      sarcasm = 'I "like" it.'

    在這個指南中,字串風格是採用前者。

  • 別用字元表示語法 ?x,從 Ruby 1.9 之後他已經是多餘的, ?x 會被解譯成 'x'(內有一個字元的字串)。 [link]

    # 不好
    char = ?c
    
    # 好
    char = 'c'
  • 別忘了使用 {} 圍繞要被插入字串的實體與全域變數。 [link]

    class Person
      attr_reader :first_name, :last_name
    
      def initialize(first_name, last_name)
        @first_name = first_name
        @last_name = last_name
      end
    
      # 差 – 有效,但難看
      def to_s
        "#@first_name #@last_name"
      end
    
      # 好
      def to_s
        "#{@first_name} #{@last_name}"
      end
    end
    
    $global = 0
    # 差
    puts "$global = #$global"
    
    # 好
    puts "$global = #{$global}"
  • 別使用 Object#to_s 在字串的插入中,他會自動調用的。 [link]

    # 不好
    message = "This is the #{result.to_s}."
    
    # 好
    message = "This is the #{result}."
  • 當你需要建構龐大的資料區段(chunk)時,避免使用 String#+ 。 使用 String#<< 來替代。字串串接在對的地方改變字串實體,並且永遠比 String#+ 來得快,String#+ 創造了一堆新的字串物件。 [link]

    # 不好
    html = ''
    html += '<h1>Page title</h1>'
    
    paragraphs.each do |paragraph|
      html += "<p>#{paragraph}</p>"
    end
    
    # 好又快
    html = ''
    html << '<h1>Page title</h1>'
    
    paragraphs.each do |paragraph|
      html << "<p>#{paragraph}</p>"
    end
  • 如果你有更好更快的選擇,別使用 String#gsub[link]

    url = 'http://example.com'
    str = 'lisp-case-rules'
    
    # 不好
    url.gsub('http://', 'https://')
    str.gsub('-', '_')
    
    # 好
    url.sub('http://', 'https://')
    str.tr('-', '_')
  • 當使用多行字串的 heredocs 時,請記得他們保留空白字符的事實, 使用一些修剪空白的邊距是很好的做法。 [link]

    code = <<-END.gsub(/^\s+\|/, '')
      |def test
      |  some_method
      |  other_method
      |end
    END
    # => "def test\n  some_method\n  other_method\nend\n"
  •   使用 Ruby 2.3 新增的<<~ 操作符來縮排 heredocs 中的多行文本。 [link]

    # 差 — 使用 Powerpack 專案的 String#strip_margin
    code = <<-RUBY.strip_margin('|')
      |def test
      |  some_method
      |  other_method
      |end
    RUBY
    
    # 差
    code = <<-RUBY
    def test
      some_method
      other_method
    end
    RUBY
    
    # 好
    code = <<~RUBY
      def test
        some_method
        other_method
      end
    RUBY
  • 為 heredocs 使用描述性分隔符並且添加有關 heredoc 內容的信息, 附帶的好處是,如果使用正確的分隔符,某些編輯器可以突出顯示 heredocs 中的程式碼。 [link]

    # 不好
    code = <<~END
      def foo
        bar
      end
    END
    
    # 好
    code = <<~RUBY
      def foo
        bar
      end
    RUBY
    
    # 好
    code = <<~SUMMARY
      An imposing black structure provides a connection between the past and
      the future in this enigmatic adaptation of a short story by revered
      sci-fi author Arthur C. Clarke.
    SUMMARY

日期與時間

  • 當需要現在系統時間時,偏好使用 Time.now 勝於 Time.new[link]

  • 除非你的計算需要考慮歷史上的曆法變革,否則避免使用 DateTime 。但如果你使用了,請確實設置 start 參數來清楚描述你的意圖。 [link]

    # 不好 - 對於現在時間使用 DateTime
    DateTime.now
    
    # 好 - 對於現在時間使用 Time
    Time.now
    
    # 不好 - 對於近代日期使用 DateTime
    DateTime.iso8601('2016-06-29')
    
    # 好 - 對於近代日期使用 Date
    Date.iso8601('2016-06-29')
    
    # 好 - 對於歷史日期使用 DateTime 且明確定義 start 參數為 ENGLAND 曆法
    DateTime.iso8601('1751-04-23', Date::ENGLAND)

正規表示法

有些人在面對問題時,不經大腦便認為,「我知道,這裡該用正規表示法」。現在問題反倒變成兩個了。
-- Jamie Zawinski

  • 如果你只需要在字串中簡單的搜索文字,不要使用正規表示法:string['text'] [link]

  • 針對簡單的字串查詢,你可以直接在字串索引中直接使用正規表示法。 [link]

    match = string[/regexp/]             # 獲得匹配正規表示法的內容
    first_group = string[/text(grp)/, 1] # 或得分組的內容
    string[/text (grp)/, 1] = 'replace'  # string => 'text replace'
  • 當你不需要替結果分組時,使用非分組的群組。 [link]

    # 不好
    /(first|second)/
    
    # 好
    /(?:first|second)/
  • 別用難以理解的老式 Perl 變數來表示最後的正規表示式匹配值(像是 $1$2 等等),使用 Regexp.last_match(n) 取代。 [link]

    /(regexp)/ =~ string
    ...
    
    # 不好
    process $1
    
    # 好
    process Regexp.last_match(1)
  • 避免使用編號群組,因為它們很難追蹤它們包含什麼。可以使用命名群組來替代。 [link]

    # 不好
    /(regexp)/ =~ string
    # 一些程式碼
    process Regexp.last_match(1)
    
    # 好
    /(?<meaningful_var>regexp)/ =~ string
    # 一些程式碼
    process meaningful_var
  • 字元類別只有幾個你需要關心的特殊字元:^, -, \, ],所以你不用逃脫字元 . 或在 [] 的中括號。 [link]

  • 小心使用 ^$ ,它們匹配的是一行的開始與結束,不是字串的開始與結束。如果你想要匹配整個字串,使用 \A\z。(譯註:\Z 實為 /\n?\z/,使用 \z 才能匹配到有含新行的字串的結束) [link]

    string = "some injection\nusername"
    string[/^username$/]   # 匹配
    string[/\Ausername\z/] # 無匹配
  • 針對複雜的正規表示法,使用 x 修飾符。這讓它們的可讀性更高並且你可以加入有用的註解。只是要小心忽略的空白。 [link]

    regexp = %r{
      start         # 一些文字
      \s            # 空白字元
      (group)       # 第一組
      (?:alt1|alt2) # 一些替代方案
      end
    }x
  • 針對複雜的替換,subgsub 可以與區塊或雜湊來使用。 [link]

    words = 'foo bar'
    words.sub(/f/, 'f' => 'F') # => 'Foo bar'
    words.gsub(/\w+/) { |word| word.capitalize } # => 'Foo Bar'

百分比字面

  • 使用 %()(比 %Q 還短) 給需要插值與嵌入雙引號的單行字串。多行字串,偏好使用 heredocs 。 [link]

    # 不好(不需要插值)
    %(<div class="text">Some text</div>)
    # 應該使用 '<div class="text">Some text</div>'
    
    # 不好(沒有雙引號)
    %(This is #{quality} style)
    # 應該使用 "This is #{quality} style"
    
    # 不好(多行)
    %(<div>\n<span class="big">#{exclamation}</span>\n</div>)
    # 應該是一個 heredoc
    
    # 好(需要插值、有雙引號以及單行)
    %(<tr><td class="name">#{name}</td>)
  • 避免使用 %() 或是 %q() 除非你的的字串中同時擁有 '"。偏好可讀性高的一般字串表示法,除非其中有太多跳脫字元。 [link]

    # 不好
    name = %q(Bruce Wayne)
    time = %q(8 o'clock)
    question = %q("What did you say?")
    
    # 好
    name = 'Bruce Wayne'
    time = "8 o'clock"
    question = '"What did you say?"'
    quote = %q(<p class='quote'>"What did you say?"</p>)
  • 正規表示法要匹配多於一個的 / 字元時,使用 %r[link]

    # 不好
    %r{\s+}
    
    # 好
    %r{^/(.*)$}
    %r{^/blog/2011/(.*)$}
  • 避免使用 %x 除非你要調用帶有 ` 的指令(這不太可能)。 [link]

    # 不好
    date = %x(date)
    
    # 好
    date = `date`
    echo = %x(echo `date`)
  • 避免使用 %s,社群似乎一致認同 :"some string" 是建立有空格符號的最佳解。 [link]

  • 偏好 () 作為各種 % 字面的最佳分隔符。 [link]

    • () 用於字串的字面表示(%q, %Q)。
    • [] 用於陣列的字面表示(%w, %i, %W, %I)如同一般陣列一樣對齊。
    • {} 用於正規表示式(%r),因為括號(())在正規表示式中經常出現,因此使用少見的 {%r 中是最好的選擇。
    • () 用於其他的字面表示(像是 %s, %x)。
    # 不好
    %q{"Test's king!", John said.}
    
    # 好
    %q("Test's king!", John said.)
    
    # 不好
    %w(one two three)
    %i(one two three)
    
    # 好
    %w[one two three]
    %i[one two three]
    
    # 不好
    %r((\w+)-(\d+))
    %r{\w{1,2}\d{2,5}}
    
    # 好
    %r{(\w+)-(\d+)}
    %r|\w{1,2}\d{2,5}|

元程式設計

  • 避免無謂的元程式設計。 [link]

  • 寫一個函式庫時不要在核心類別搗亂(不要替它們加 monkey patch。) [link]

  • 偏好區塊形式的 class_eval 勝於字串插值 (string-interpolated)的形式。 [link]

    • 當你使用字串插值形式時,總是提供 __FILE____LINE__,使你的 backtrace 看起來有意義:

      class_eval "def use_relative_model_naming?; true; end", __FILE__, __LINE__
    • 偏好 define_method 勝於 class_eval{ def ... }

  • 當使用 class_eval (或其它的 eval)搭配字串插值時,添加一個註解區塊,來顯示如果做了插值的樣子(我從 Rails 程式碼學來的一個實踐): [link]

    # 從 activesupport/lib/active_support/core_ext/string/output_safety.rb
    UNSAFE_STRING_METHODS.each do |unsafe_method|
      if 'String'.respond_to?(unsafe_method)
        class_eval <<-EOT, __FILE__, __LINE__ + 1
          def #{unsafe_method}(*args, &block)       # def capitalize(*args, &block)
            to_str.#{unsafe_method}(*args, &block)  #   to_str.capitalize(*args, &block)
          end                                       # end
    
          def #{unsafe_method}!(*args)              # def capitalize!(*args)
            @dirty = true                           #   @dirty = true
            super                                   #   super
          end                                       # end
        EOT
      end
    end
  • 元程式設計避免使用 method_missing。會讓 Backtraces 變得很凌亂;行為沒有列在 #methods 裡;拼錯的方法呼叫可能默默的工作(nukes.launch_state = false)。考慮使用 delegation, proxy, 或是 define_method 來取代。如果你必須使用 method_missing[link]

    • 確保也定義了 respond_to_missing?

    • 僅捕捉字首定義良好的方法,像是 find_by_* ― 讓你的程式碼愈肯定(assertive)愈好。

    • 在最後的敘述句(statement)呼叫 super

    • 從 delegate 到 assertive, 不神奇的(non-magical)方法:

      # 不好
      def method_missing?(meth, *args, &block)
        if /^find_by_(?<prop>.*)/ =~ meth
          # ... lots of code to do a find_by
        else
          super
        end
      end
      
      # 好
      def method_missing?(meth, *args, &block)
        if /^find_by_(?<prop>.*)/ =~ meth
          find_by(prop, *args, &block)
        else
          super
        end
      end
      
      # 而最好是在每個可找到的屬性被宣告時,使用 define_method。
  • 偏好使用 public_send 更勝於 send,如此一來可以避開 private/protected 的洩漏。 [link]

    # 我們有一個包含 Activatable 模組的 ActiveModel Organization
    module Activatable
      extend ActiveSupport::Concern
    
      included do
        before_create :create_token
      end
    
      private
    
      def reset_token
        # 一些程式碼
      end
    
      def create_token
        # 一些程式碼
      end
    
      def activate!
        # 一些程式碼
      end
    end
    
    class Organization < ActiveRecord::Base
      include Activatable
    end
    
    linux_organization = Organization.find(...)
    # 不好 - 私有方法被使用
    linux_organization.send(:reset_token)
    # 好 - 會拋出例外
    linux_organization.public_send(:reset_token)
  • 偏好 __send__ 勝於 send,後者可能會與現有方法重疊。 [link]

    require 'socket'
    
    u1 = UDPSocket.new
    u1.bind('127.0.0.1', 4913)
    u2 = UDPSocket.new
    u2.connect('127.0.0.1', 4913)
    # 並不會傳遞訊息給接收器物件
    # 取而代之的是他會經由 UDP socket 傳遞訊息
    u2.send :sleep, 0
    # 以下的方法才會確實的傳遞訊息給接收器物件
    u2.__send__ ...

其它

  • ruby -w 寫出安全的程式碼。 [link]

  • 避免使用雜湊作為選擇性參數。這個方法是不是做太多事情了?(物件初始器是本規則的例外) [link]

  • 避免方法長於 10 行程式碼(LOC)。理想上,大部分的方法會小於5行。空行不算進 LOC 裡。 [link]

  • 避免參數列表長於三或四個參數。 [link]

  • 如果你真的需要全域方法,把它們加入到 Kernel 並設為私有的。 [link]

  • 使用模組變數取代全域變數。 [link]

    # 不好
    $foo_bar = 1
    
    # 好
    module Foo
      class << self
        attr_accessor :bar
      end
    end
    
    Foo.bar = 1
  • 使用 OptionParser 來解析複雜的命令行選項及 ruby -s 來處理瑣碎的命令行選項。 [link]

  • 用函數式的方法寫程式,在有意義的情況下避免賦值。 [link]

  • 不要變動參數,除非那是方法的目的。 [link]

  • 避免超過三行的巢狀區塊。 [link]

  • 保持一致性。在理想的世界裡,遵循這些準則。 [link]

  • 使用常識。 [link]

工具

以下是一些工具,讓你自動檢查 Ruby 程式碼是否符合本指南。

RuboCop

RuboCop 是一個基於本指南的 Ruby 語言風格檢查器,幾乎包含了此指南重要的部分,支援 MRI 1.9、MRI 2.0 還有良好的 Emacs 協作。

RubyMine

RubyMine的程式碼檢查部分基於本指南。

貢獻

本指南仍處於不斷更新的狀態 — 有些規則缺乏範例、有些規則則是缺乏清楚的例子來說明他,協助改進這些規則是一個很好(同時也簡單)的方法來幫助 Ruby 社群。

時機成熟時,(希望)這些問題將會被解決 — 記著吧!

在本指南所寫的每個東西都不是定案。這只是我渴望想與同樣對 Ruby 程式設計風格有興趣的大家一起工作,以致於最終我們可以替整個 Ruby 社群創造一個有益的資源。

歡迎開票或發送一個帶有改進的更新請求。在此提前感謝你的幫助!

你同時也可以透過Gratipay來為本指南(還有 RuboCop)提供一些財務上的貢獻。

Support via Gratipay

如何貢獻?

真的很簡單,只要照著貢獻指南即可。

授權

Creative Commons License This work is licensed under a Creative Commons Attribution 3.0 Unported License

口耳相傳

一份社群策動的風格指南,對一個社群來說,只是讓人知道有這個社群。推特這個指南,分享給你的朋友或同事。我們得到的每個註解、建議或意見都可以讓這份指南變得更好一點。而我們想要擁有的是最好的指南,不是嗎?

共勉之,
Bozhidar