Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Refactoring (Ruby edition)

Refactoring (Ruby edition)

高見龍

May 31, 2018
Tweet

More Decks by 高見龍

Other Decks in Programming

Transcript

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

    View Slide

  2. a.k.a Eddie
    愛現! 喜歡冷門的玩具
    Ruby/Rails/iOS app 開發者、講師
    Ruby 技術推廣、教育、諮詢
    台灣、日本等國內外 Ruby 技術研討會講者
    目前於五倍紅寶石擔任紅寶石鑑定商職務
    部落格:https://kaochenlong.com
    高見龍
    photo by Eddie
    @eddiekao

    View Slide

  3. 發售中!
    https://railsbook.tw/

    View Slide

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

    View Slide

  5. • 什麼是重構?
    • 動手作! (50 mins)
    • 何時該進行重構?
    • 何時不該進行重構?
    • 程式碼的壞味道
    • 常見重構手法介紹
    • Ruby 風味之程式整理手法

    View Slide

  6. 參考資料

    View Slide

  7. 什麼是重構?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. 重構有風險

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. 動手做!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. 重構不會改變軟體

    可受觀察之行為

    View Slide

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

    View Slide

  23. 重構 101

    View Slide

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

    View Slide

  25. 何時該進行重構?

    View Slide

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

    View Slide

  27. 不要為重構而重構

    View Slide

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

    View Slide

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

    View Slide

  30. 事不過三原則

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  34. 何時不該進行重構?

    View Slide

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

    View Slide

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

    View Slide

  37. 程式碼的壞味道

    View Slide

  38. 知道 How,也要知道 When

    View Slide

  39. 壞味道

    View Slide

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

    View Slide

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

    View Slide

  42. 壞味道:
    過大的類別
    large class
    可用手法:
    • Extract Class

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  47. 常見重構手法介紹

    View Slide

  48. 提煉方法
    Extract Method
    • 一個方法做太多事
    • 方法的長度不是問題,問題在於方法本身是
    不是能明確表達它要做的事
    • 用「做什麼」,不要用「怎麼做」來命名
    • 區域變數有時也要一併傳進新的方法

    View Slide

  49. def print_owing(amount)
    print_banner
    puts "name: #{@name}"
    puts "amount: #{amount}"
    end
    使⽤用前

    View Slide

  50. def print_owing(amount)
    print_banner
    print_details amount
    end
    def print_details(amount)
    puts "name: #{@name}"
    puts "amount: #{amount}"
    end
    使⽤用後

    View Slide

  51. 方法內聯化
    Inline Method
    • 一行程式就能做完的,不一定要另外給它一
    個方法
    • 如果方法本身就能明確表達它的意思,不用
    特別拆成兩個。

    View Slide

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

    View Slide

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

    View Slide

  54. 以查詢取代暫時變數
    Replace Temp with Query
    • 暫時變數就只是暫時的...
    • 如果暫時變數只被賦值一次
    • 效能問題?

    View Slide

  55. base_price = @quantity * @item_price
    if base_price > 1000
    base_price * 0.95
    else
    base_price * 0.98
    end
    使⽤用前

    View Slide

  56. if base_price > 1000
    base_price * 0.95
    else
    base_price * 0.98
    end
    def base_price
    @quantity * @item_price
    end
    使⽤用後

    View Slide

  57. 以方法鍊結取代暫時變數
    Replace Temp with Chain
    • 方法執行後回傳物件
    • 在 Ruby / Rails 常見,可減少程式碼行數。

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  64. if (platform.upcase.index("MAC") &&
    browser.upcase.index("IE") &&
    initialized? &&
    resize > 0
    )
    # do something
    end
    使⽤用前

    View Slide

  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
    使⽤用後

    View Slide

  66. 剖解暫時變數
    Split Temporary Variable
    • 不要讓同一個暫存變數做不同的用途

    View Slide

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

    View Slide

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

    View Slide

  69. 以方法物件取代方法
    Replace Method with Method Object
    • 有時候方法太複雜,無法用 Extract Method
    或 Replace Temp with Query 手法處理…
    • 新增一個類別,把方法包進去

    View Slide

  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
    使⽤用前

    View Slide

  71. class Account
    def gamma(input_val, quantity, year_to_date)
    Gamma.new(self, input_val, quantity,
    year_to_date).compute
    end
    end
    使⽤用後

    View Slide

  72. 以集合閉包方法取代迴圈
    Replace Loop with Collection Closure Method
    • 可縮短程式碼行數,但不見得保證能增加程
    式碼可讀性
    • 不是每種程式語言都有支援

    View Slide

  73. managers = []
    employees.each do |e|
    managers << e if e.manager?
    end
    使⽤用前

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  78. 重新命名方法
    Rename Method
    • 命名需要經驗,還有英文字要認識的夠多
    • 通常無法第一次就給方法取個好名字
    • 在舊方法呼叫新方法
    • 舊的方法不要砍,只要先標記 deprecated

    View Slide

  79. View Slide

  80. 搬移方法
    Move Method
    • 如果一個類別有太多方法,或是與另一個類
    別黏太緊...
    • 有時不容易下決定,需要經驗值!
    • 適當使用委託方法(delegation method)

    View Slide

  81. View Slide

  82. 搬移欄位
    Move Field
    • 一開始的設計不良...
    • 如果是 public field,可先寫個 public getter/
    setter 來包裝它

    View Slide

  83. View Slide

  84. 提煉類別
    Extract Class
    • 每個類別應該都有它明確的責任
    • 隨著時間,責任越來越重,這樣不太好...
    • 分解每個類別的責任
    • 決定是否讓新的類別曝光,如果不要,可考
    慮使用委託方法(delegation method)

    View Slide

  85. View Slide

  86. 自我封裝欄位
    Self Encapsulate Field
    • 使用 getter/setter 來取用實體變數
    • Lazy initialization(要用的時候才初始化)

    View Slide

  87. class Price
    def total
    @base_price * (1 + @tax_rate)
    end
    end
    使⽤用前

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  91. if modifier

    View Slide

  92. open class

    View Slide

  93. dynamic method

    View Slide

  94. Dynamic Method 1/5

    View Slide

  95. DRY = Don’t Repeat Yourself

    View Slide

  96. Dynamic Method 2/5

    View Slide

  97. Dynamic Method 3/5

    View Slide

  98. Dynamic Method 4/5

    View Slide

  99. Dynamic Method 5/5

    View Slide

  100. Method Missing 1/3

    View Slide

  101. Method Missing 2/3

    View Slide

  102. Method Missing 3/3

    View Slide

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

    View Slide

  104. 高見龍 Blog
    Facebook
    Twitter
    Email
    Mobile
    https://kaochenlong.com
    https://www.facebook.com/eddiekao
    https://twitter.com/eddiekao
    [email protected]
    +886-928-617-687

    View Slide