Refactoring (Ruby edition)

Refactoring (Ruby edition)

F2dcf6633971844e19ca96ea294ba976?s=128

高見龍

May 31, 2018
Tweet

Transcript

  1. 從好到更好 高見龍 @ 五倍紅寶石 程式碼重構

  2. a.k.a Eddie 愛現! 喜歡冷門的玩具 Ruby/Rails/iOS app 開發者、講師 Ruby 技術推廣、教育、諮詢 台灣、日本等國內外

    Ruby 技術研討會講者 目前於五倍紅寶石擔任紅寶石鑑定商職務 部落格:https://kaochenlong.com 高見龍 photo by Eddie @eddiekao
  3. 發售中! https://railsbook.tw/

  4. 發售中! https://gitbook.tw/

  5. • 什麼是重構? • 動手作! (50 mins) • 何時該進行重構? • 何時不該進行重構?

    • 程式碼的壞味道 • 常見重構手法介紹 • Ruby 風味之程式整理手法
  6. 參考資料

  7. 什麼是重構?

  8. 程式碼會慢慢腐爛...

  9. 不是每個專案都能砍掉重練!

  10. 重構: 「在不改變程式碼外在行為的 前提下,對程式碼做出修改, 以改進程式的內部結構」

  11. 重構不是藝術,重構是工程

  12. 重構有風險

  13. 編譯器不會在乎你的程式碼好不好看

  14. 如果沒壞,就別動它? 它是沒壞,但會讓你的生活變得比較難過...

  15. 先決條件:測試 只要是人,都會犯錯

  16. 動手做!

  17. Let’s Coding https://github.com/kaochenlong/code_refactoring_demo

  18. 重構的節奏 小修改→測試→小修改→測試→小修改→測試...

  19. 重構就只是整理程式碼?

  20. 重構的目的是使軟體更容易被理解 及修改。

  21. 重構不會改變軟體 「 可受觀察之行為 」

  22. 「我不是個偉大的程式員,我只是個 有著一些優秀習慣的好程式員而已」 - Kent Beck

  23. 重構 101

  24. 下手的好對象: • 區域變數 • 迴圈 • 條件式 • 註解

  25. 何時該進行重構?

  26. 重構不是一件需要特別撥 出時間來做的事情

  27. 不要為重構而重構

  28. 加新功能、修正錯誤、Code review 時都可進行重構。

  29. 重構有助於了解別人(或不 久前自己)的程式碼。

  30. 事不過三原則

  31. 要怎麼說服你的長官 進行重構?

  32. 如果講不聽,你就自己做吧 畢竟最後還是你要來維護這個專案

  33. 程式碼被閱讀及修改的次數, 遠多於它被編寫的次數。

  34. 何時不該進行重構?

  35. 需要砍掉重練的時候!

  36. 時間剩下不多的時候!

  37. 程式碼的壞味道

  38. 知道 How,也要知道 When

  39. 壞味道

  40. 壞味道: 重複的程式碼 duplicated code 可用手法: • Extract Method • Pull

    Up Method
  41. 壞味道: 過長的方法 long method 可用手法: • Extract Method • Replace

    Temp with Query • Replace Method with Method Object
  42. 壞味道: 過大的類別 large class 可用手法: • Extract Class

  43. 壞味道: 一個方法有過多的參數 long parameter list 可用手法: • Replace Parameter with

    Method
  44. 壞味道: Switch 現身 switch statements 可用手法: • Replace Type Code

    with Polymorphism
  45. 壞味道: 暫時欄位 temporary field 可用手法: • Extract Class • Replace

    Method with Method Object
  46. 壞味道: 過多的註解 comments 可用手法: • Extract Method • Rename Method

  47. 常見重構手法介紹

  48. 提煉方法 Extract Method • 一個方法做太多事 • 方法的長度不是問題,問題在於方法本身是 不是能明確表達它要做的事 • 用「做什麼」,不要用「怎麼做」來命名

    • 區域變數有時也要一併傳進新的方法
  49. def print_owing(amount) print_banner puts "name: #{@name}" puts "amount: #{amount}" end

    使⽤用前
  50. def print_owing(amount) print_banner print_details amount end def print_details(amount) puts "name:

    #{@name}" puts "amount: #{amount}" end 使⽤用後
  51. 方法內聯化 Inline Method • 一行程式就能做完的,不一定要另外給它一 個方法 • 如果方法本身就能明確表達它的意思,不用 特別拆成兩個。

  52. def get_rating more_than_five_late_deliveries ? 2 : 1 end def more_than_five_late_deliveries

    @number_of_late_deliveries > 5 end 使⽤用前
  53. def get_rating @number_of_late_deliveries > 5 ? 2 : 1 end

    使⽤用後
  54. 以查詢取代暫時變數 Replace Temp with Query • 暫時變數就只是暫時的... • 如果暫時變數只被賦值一次 •

    效能問題?
  55. base_price = @quantity * @item_price if base_price > 1000 base_price

    * 0.95 else base_price * 0.98 end 使⽤用前
  56. if base_price > 1000 base_price * 0.95 else base_price *

    0.98 end def base_price @quantity * @item_price end 使⽤用後
  57. 以方法鍊結取代暫時變數 Replace Temp with Chain • 方法執行後回傳物件 • 在 Ruby

    / Rails 常見,可減少程式碼行數。
  58. mock = Mock.new expectation = mock.expects(:a_method_name) expectation.with("arguments") expectation.returns([1, :array]) 使⽤用前

  59. mock = Mock.new mock.expects(:a_method_name).with("arguments" ).returns([1, :array]) 使⽤用後

  60. 暫時變數內聯化 Inline Temp • 通常是 Replace Temp with Query 手法的一部

    份。
  61. base_price = an_order.base_price return base_price > 1000 使⽤用前

  62. return an_order.base_price > 1000 使⽤用後

  63. 引入解釋性變數 Introduce Explaining Variable • 把複雜的運算放進暫時變數,用變數名稱來 解釋其用途 • 也可使用 Extract

    Method 來取代之
  64. if (platform.upcase.index("MAC") && browser.upcase.index("IE") && initialized? && resize > 0

    ) # do something end 使⽤用前
  65. is_mac_os = platform.upcase.index("MAC") is_ie_browser = browser.upcase.index("IE") was_resized = resize >

    0 if (is_mac_os && is_ie_browser && initialized? && was_resized) # do something end 使⽤用後
  66. 剖解暫時變數 Split Temporary Variable • 不要讓同一個暫存變數做不同的用途

  67. temp = 2 * (@height + @width) puts temp temp

    = @height * @width puts temp 使⽤用前
  68. perimeter = 2 * (@height + @width) puts perimeter area

    = @height * @width puts area 使⽤用後
  69. 以方法物件取代方法 Replace Method with Method Object • 有時候方法太複雜,無法用 Extract Method

    或 Replace Temp with Query 手法處理… • 新增一個類別,把方法包進去
  70. class Account def gamma(input_val, quantity, year_to_date) important_value1 = (input_val *

    quantity) + delta important_value2 = (input_val * year_to_date) + 100 if (year_to_date - important_value1) > 100 important_value2 -= 20 end important_value3 = important_value2 * 7 # and so on. important_value3 - 2 * important_value1 end end 使⽤用前
  71. class Account def gamma(input_val, quantity, year_to_date) Gamma.new(self, input_val, quantity, year_to_date).compute

    end end 使⽤用後
  72. 以集合閉包方法取代迴圈 Replace Loop with Collection Closure Method • 可縮短程式碼行數,但不見得保證能增加程 式碼可讀性

    • 不是每種程式語言都有支援
  73. managers = [] employees.each do |e| managers << e if

    e.manager? end 使⽤用前
  74. managers = employees.select {|e| e.manager?} 使⽤用後

  75. 方法上移 • DRY = Don’t Repeat Yourself • 把子類別重複的方法提到上層父類別 Pull

    Up Method
  76. class Person attr_reader :first_name, :last_name def initialize first_name, last_name @first_name

    = first_name @last_name = last_name end end class Male < Person def full_name first_name + " " + last_name end def gender "M" end end 使⽤用前 class Female < Person def full_name first_name + " " + last_name end def gender "F" end end
  77. class Person attr_reader :first_name, :last_name def initialize first_name, last_name @first_name

    = first_name @last_name = last_name end def full_name first_name + " " + last_name end end class MalePerson < Person def gender "M" end end 使⽤用後 class FemalePerson < Person def gender "F" end end
  78. 重新命名方法 Rename Method • 命名需要經驗,還有英文字要認識的夠多 • 通常無法第一次就給方法取個好名字 • 在舊方法呼叫新方法 •

    舊的方法不要砍,只要先標記 deprecated
  79. None
  80. 搬移方法 Move Method • 如果一個類別有太多方法,或是與另一個類 別黏太緊... • 有時不容易下決定,需要經驗值! • 適當使用委託方法(delegation

    method)
  81. None
  82. 搬移欄位 Move Field • 一開始的設計不良... • 如果是 public field,可先寫個 public

    getter/ setter 來包裝它
  83. None
  84. 提煉類別 Extract Class • 每個類別應該都有它明確的責任 • 隨著時間,責任越來越重,這樣不太好... • 分解每個類別的責任 •

    決定是否讓新的類別曝光,如果不要,可考 慮使用委託方法(delegation method)
  85. None
  86. 自我封裝欄位 Self Encapsulate Field • 使用 getter/setter 來取用實體變數 • Lazy

    initialization(要用的時候才初始化)
  87. class Price def total @base_price * (1 + @tax_rate) end

    end 使⽤用前
  88. class Price attr_reader :base_price, :tax_rate def total base_price * (1

    + tax_rate) end end 使⽤用後
  89. 重構: 「在不改變程式碼外在行為的 前提下,對程式碼做出修改, 以改進程式的內部結構」

  90. Ruby 風味之程式整理手法

  91. if modifier

  92. open class

  93. dynamic method

  94. Dynamic Method 1/5

  95. DRY = Don’t Repeat Yourself

  96. Dynamic Method 2/5

  97. Dynamic Method 3/5

  98. Dynamic Method 4/5

  99. Dynamic Method 5/5

  100. Method Missing 1/3

  101. Method Missing 2/3

  102. Method Missing 3/3

  103. https://github.com/bbatsov/rubocop

  104. 高見龍 Blog Facebook Twitter Email Mobile https://kaochenlong.com https://www.facebook.com/eddiekao https://twitter.com/eddiekao eddie@5xruby.tw

    +886-928-617-687