重構小技巧 in Ruby
By Alan Chou・Dec 29 2017・ 技術文章
本文內容主要參閱 Refactoring: Ruby Edition: Ruby Edition (Addison-Wesley Professional Ruby Series)
什麼是重構?
重構的過程不應改變程式碼原有的行為,重構時只應以下列三點為考量:
- 讓程式碼更好被閱讀。
- 使程式邏輯更清晰。
- 使程式更容易被修改。
最終提升程式得以持續開發的速度,此亦為重構的主要目的。
為何需要重構?
- 程式碼閱讀性不佳,每次修改時都需要重新理解(通靈)原作者的思維(天書),花費額外的時間成本。
- 相同的程式邏輯散落地出現在多處不同的地方時,修改時要再三確認是否每一段相同但在不同地方的程式碼都改到了。
- 需要為現有的程式碼加入新的行為或修正,但突然發現加不進去或者不知道該怎麼加。
- 程式碼包含複雜的邏輯與條件,使得新的修改造成更多的問題或錯誤的結果。
重構的最佳時機
當你要對現有的程式碼加入一個新的 Function 或者要修正一個 Bug 時,但你發現原有的程式碼難以閱讀,因此難以修改時,此為最佳的重構時機。
重構的基本流程
- 確認單元測試的完整性,沒有測試的程式碼不應輕易重構。
- 重構原有的程式碼符合原有的行為及邏輯。
- 然後加入新的 Function 或進行 Bug Fix 。
何時你不該重構?
- 當你準備放棄原有的程式碼,決定完全重寫時:可能原本的程式就是一個錯誤(或者充滿太多錯誤時),沒有重構的價值。值得重構的程式碼應該是大部份的功能與邏輯都正確,只是樣子醜了點罷了。
- 沒有測試的程式碼,不應重構,應該先補上程式行為 (behavior) 的測試,再開始重構。
- 死線 (dealine) 在即,因為你沒有時間了,重構是程式設計師間的浪漫,非程式設計師是很難(可說是無法)理解的。
Creating a beautiful code base should always be a priority; however, creating working software’s the number one priority
- 單純為了重構而重構 。 > The most costly refacoring is refactoring for academic purposes.
If you can’t prove to the business and the project manager that a refactoring is worth-doing, you might be refactoring for academic purposes refactoring.
重構可由下列四個面向來執行
- 整理共享的邏輯 (To enable sharing of logic)。
- 將目的與實作細節分開來解釋,特別是在命名方法名稱的時候 (To explain intention and implementation separately)。
- 隔離容易變動的程式碼,最簡單且基礎的方式就是把易變動的部份關在私有方法中,限制被存取的範圍 (To isolate change)。
- 將複雜的條件 (conditionals) 轉化為簡明的訊息(messages)傳遞 (To encode conditional logic)。
本文僅以重複的程式碼 (Duplicated Code) 為探討主題
Duplicated Code 大概有下列三種情況:
- 相同的程式邏輯出現在多個方法間。
- 子類別間重複的程式碼。
- 不相干類別間重複的程式碼。
1. 相同的程式邏輯出現在多個方法中時
- 可採用 Extract Method 技巧,把每個重複的程式段落,轉化為一個個的小方法,方法名稱必須要明確指定方法的目的,理想的方法名稱,是光看方法名稱就知道該方法要做什麼。
#
# Extract Method
#
# 重構前
#
def flight_tickets
info = []
info << outbound_flight_info
info << return_flight_info
info << total_fees
info.join("\n")
end
def outbound_flight_info
info = []
info << "Outbound"
info << "From: Taipei(TPE)"
info << "To: Tokyo(NRT)"
info << "Flight: IT200"
info << "Airline: Tigerair Taiwan"
info << "Date: Tue, Nov 28, 2017"
info << "Departure time: 06:35 AM"
info << "Arrival time: 10:30 AM"
info << "Journey duration: 2h 55m"
info << "Terminal: T1"
info << "Non-stop"
info << "Basic economy"
info << "Fare: TWD 7,998"
info << "Fees & Taxes: TWD 1,000"
info << "2 adults"
end
def return_flight_info
info = []
info << "Return"
info << "From: Tokyo(NRT)"
info << "To: Taipei(TPE)"
info << "Flight: IT201"
info << "Airline: Tigerair Taiwan"
info << "Date: Fri, Dec 1, 2017"
info << "Departure time: 01:00 PM"
info << "Arrival time: 04:20 PM"
info << "Journey duration: 4h 20m"
info << "Terminal: T2"
info << "Non-stop"
info << "Basic economy"
info << "Fare: TWD 3,398"
info << "Fees & Taxes: TWD 1,378"
info << "2 adults"
end
def total_fees
"Total: TWD 13,774"
end
puts flight_tickets
#
# Extract Method
#
# 重構後
# 這邊初步的重構重點只是單純的把共享的邏輯整理成一個個簡單的私有(private)方法
# 增加程式的閱讀性
#
require 'date'
require 'time'
def flight_tickets
info = []
info << outbound_flight_info
info << return_flight_info
info << total_fees(13_774)
info.join("\n")
end
def outbound_flight_info
info = []
info << "Outbound"
info << from("Taipei", "TPE")
info << to("Tokyo", "NRT")
info << flight_number("IT200")
info << airline("Tigerair Taiwan")
info << flight_date(2017, 11, 28)
info << departure_time(6, 35)
info << arrival_time(10, 30)
info << journey_duration(2, 55)
info << terminal(1)
info << non_stop
info << fare_class
info << fare(7_998)
info << fees_and_taxs(1_000)
info << adult_travelers(2)
end
def return_flight_info
info = []
info << "Return"
info << from("Tokyo", "NRT")
info << to("Taipei", "TPE")
info << flight_number("IT201")
info << airline("Tigerair Taiwan")
info << flight_date(2017, 12, 1)
info << departure_time(13, 00)
info << arrival_time(16, 20)
info << journey_duration(4, 20)
info << terminal(2)
info << non_stop
info << fare_class
info << fare(3_398)
info << fees_and_taxs(1_378)
info << adult_travelers(2)
end
private
def from(city, airport_code)
"From: #{city}(#{airport_code})"
end
def to(city, airport_code)
"To: #{city}(#{airport_code})"
end
def total_fees(amount)
"Total: TWD #{number_delimiter(amount)}"
end
def flight_number(number)
"Flight: #{number}"
end
def airline(airline_name)
"Airline: #{airline_name}"
end
def flight_date(year, month, day)
"Date: #{Date.new(year, month, day).strftime('%a, %b %-d, %Y')}"
end
def departure_time(hour, minute)
"Departure time: #{time_formater(hour, minute)}"
end
def arrival_time(hour, minute)
"Arrival time: #{time_formater(hour, minute)}"
end
def journey_duration(hours, minutes)
"Journey duration: #{hours}h #{minutes}m"
end
def terminal(number)
"Terminal: T#{number}"
end
def non_stop
"Non-stop"
end
def fare_class
"Basic economy"
end
def fare(amount)
"Fare: TWD #{number_delimiter(amount)}"
end
def fees_and_taxs(amount)
"Fees & Taxes: TWD #{number_delimiter(amount)}"
end
def adult_travelers(amount)
"#{amount} adult" << (amount > 1 ? 's' : '')
end
def time_formater(hour, minute)
Time.parse("#{hour}:#{minute}").strftime('%I:%M %p')
end
def number_delimiter(number)
parts = number.to_s.split('.')
parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
parts.join('.')
end
puts flight_tickets
2. 子類別間重複的程式碼
- 完全相同的程式邏輯
- 使用上述 Extract Method 技巧。
- Pull Up Method 技巧:把分佈在子類別間,但執行結果完全相同的方法,往上層 (superclass) 類別移動。
- 相似但不完全相同的程式邏輯
- 對相同的部份進行 Extract Method 。
- 對不同的部份改寫成 Form Template Method,在上層定義相同的方法名稱,使其接受一致的參數規格 (signature),但實作的內容不同,把實作的細節(步驟)保留給各子類中去各自去處理,可以有下列兩種方式來實作:
- 傳統的繼承式樣版方法 (Template Method Using Inheritance)
- 擴充模組式的樣版方法 (Template Method Using Extension of Modules):可避免因繼承所造成的複雜度
下列用簡易版的機票價格做為解釋範例
#
# 傳統的繼承式樣版方法
# Template Method Using Inheritance
#
class Ticket
def price
(fare + add_on_fees) * tax_rate
end
end
class FirstClassTicket < Ticket
def fare
1000
end
def add_on_fees
200
end
def tax_rate
1.5
end
end
class BusinessClassTicket < Ticket
def fare
500
end
def add_on_fees
100
end
def tax_rate
1.33
end
end
class EconomyClassTickets < Ticket
def fare
100
end
def add_on_fees
20
end
def tax_rate
1.1
end
end
# 需要新增超級早島票的票種時(不改變方法名稱)
class SuperEarilyBirdTicket
def price
(fare + add_on_fees) * 0.5 * tax_rate
end
end
class SuperEarilyBirdFirstClassTicket < SuperEarilyBirdTicket
def fare
1000
end
def add_on_fees
200
end
def tax_rate
1.5
end
end
class SuperEarilyBirdBusinessClassTicket < SuperEarilyBirdTicket
def fare
500
end
def add_on_fees
100
end
def tax_rate
1.33
end
end
class SuperEarilyBirdEconomyClassTickets < SuperEarilyBirdTicket
def fare
100
end
def add_on_fees
20
end
def tax_rate
1.1
end
end
puts FirstClassTicket.new.price
puts BusinessClassTicket.new.price
puts EconomyClassTickets.new.price
puts SuperEarilyBirdFirstClassTicket.new.price
puts SuperEarilyBirdBusinessClassTicket.new.price
puts SuperEarilyBirdEconomyClassTickets.new.price
#
# 擴充模組式的樣版方法
# Template Method Using Extension of Modules
#
class Ticket
def price
(fare + add_on_fees) * tax_rate
end
end
module FirstClassTicket
def fare
1000
end
def add_on_fees
200
end
def tax_rate
1.5
end
end
module BusinessClassTicket
def fare
500
end
def add_on_fees
100
end
def tax_rate
1.33
end
end
module EconomyClassTickets
def fare
100
end
def add_on_fees
20
end
def tax_rate
1.1
end
end
class TicketFare
def first_class
Ticket.new.extend(FirstClassTicket).price
end
def business_class
Ticket.new.extend(BusinessClassTicket).price
end
def economy_class
Ticket.new.extend(EconomyClassTickets).price
end
def super_earily_bird_first_class
SuperEarilyBirdTicket.new.extend(FirstClassTicket).price
end
def super_earily_bird_business_class
SuperEarilyBirdTicket.new.extend(BusinessClassTicket).price
end
def super_earily_bird_economy_class
SuperEarilyBirdTicket.new.extend(EconomyClassTickets).price
end
end
# 需要新增超級早島票的票種時
class SuperEarilyBirdTicket
def price
(fare + add_on_fees) * 0.5 * tax_rate
end
end
puts TicketFare.new.first_class
puts TicketFare.new.business_class
puts TicketFare.new.economy_class
puts TicketFare.new.super_earily_bird_first_class
puts TicketFare.new.super_earily_bird_business_class
puts TicketFare.new.super_earily_bird_economy_class
- 方法內的程式碼,可以使用不同(更佳)的演算法來實作時
- Substitute Algorithm:簡單說就是改良原有的程式碼,以更好的方式來實作,讓程式碼更乾淨。
- 對出現在方法內的相同部份做 Extract Surrounding Method:假設有兩個方法的實作幾乎一樣,只有中間的一小部份不相同時,可把不同的部份重構為可接受 block 的方法。
#
# Substitute Algorithm
#
# 重構前
#
def booking_code(code)
if code == 'F'
'First class'
elsif code == 'C'
'Business class'
elsif code == 'Y'
'Economy class'
else
raise "Booking code is incorrect"
end
end
#
# Substitute Algorithm
#
# 重構後
#
CODES = {
"F" => "First class",
"C" => "Business class",
"Y" => "Economy class"
}
def booking_code(code)
CODES.fetch(code)
rescue
raise "Booking code is incorrect"
end
puts booking_code("F")
#
# Extract Surrounding Method
#
# 重構前
#
def first_class_services
puts "\n"
puts "-" * 25
puts __method__.upcase
puts "-" * 25
puts "Fine Dinning options"
puts "In-Seat Power: power outlet"
puts "Personalize Service"
puts "Complimentary Non-Alcoholic Beverages"
puts "Wi-Fi for Purchase"
puts "In-Seat Power: USB PORT"
puts "\n"
end
def business_class_service
puts "\n"
puts "-" * 25
puts __method__.upcase
puts "-" * 25
puts "Premium Snacks"
puts "In-Seat Power: power outlet"
puts "Complimentary Non-Alcoholic Beverages"
puts "Wi-Fi for Purchase"
puts "In-Seat Power: USB PORT"
puts "\n"
end
def economy_class_services
puts "\n"
puts "-" * 25
puts __method__.upcase
puts "-" * 25
puts "Snacks, Meals"
puts "Complimentary Non-Alcoholic Beverages"
puts "Wi-Fi for Purchase"
puts "In-Seat Power: USB PORT"
puts "\n"
end
first_class_services
business_class_service
economy_class_services
# 機上服務參考資料: https://www.delta.com/content/www/en_US/traveling-with-us/onboard-experience/first-class.html
#
# Extract Surrounding Method
#
# 重構後
#
def first_class_services
onboard_experience do
puts "Fine Dinning options"
puts "In-Seat Power: power outlet"
puts "Personalize Service"
end
end
def business_class_service
onboard_experience do
puts "Premium Snacks"
puts "In-Seat Power: power outlet"
end
end
def economy_class_services
onboard_experience do
puts "Snacks, Meals"
end
end
def onboard_experience
puts "\n"
puts "-" * 25
puts caller_locations(1,1)[0].label.upcase
puts "-" * 25
yield
puts "Complimentary Non-Alcoholic Beverages"
puts "Wi-Fi for Purchase"
puts "In-Seat Power: USB PORT"
ensure
puts "\n"
end
first_class_services
business_class_service
economy_class_services
3. 不相干類別間重複的程式碼
- Extract Class:將放在一起顯得很合理的部份,抽取出來成為一個獨立的類別。
- Extract Module:把相同行為的方法行為,從不同的類別中移到一個新的模組中 (Module),再讓這些類別去引入 (Include)。
結語
本文直接使用程式碼解釋重構的流程,是因為許多重構的技巧其實透過程式碼本身來解釋最為直接,若需要更詳盡的解釋,推薦可直接閱讀原作,原作中有大量的程式範例與重構步驟可供參考,除了本文介紹到的 Duplicated Code 之重構技巧外,還有其他 24 個 Bad Smells in Code 的重構因應之道。
- Long Method
- Large Class
- Long Parameter List
- Divergent Change
- Shotgun Surgery
- Feature Envy
- Data Clumps
- Primitive Obsession
- Case Statements
- Parallel Inheritance Hierarchie
- Lazy Class
- Speculative Generality
- Temporary Field
- Message Chains
- Middle Man
- Inappropriate Intimacy
- Alternative Classes with Different Interfaces
- Incomplete Library Class
- Data Class
- Refused Bequest
- Comments
- Metaprogramming Madness
- Disjointed API
- Repetitive Boilerplate