Ruby代码重构的常见模式与技巧
2021-03-284.4k 阅读
一、引言
在软件开发的生命周期中,代码重构是一项至关重要的活动。随着项目的演进,代码库可能会变得复杂、难以维护和扩展。Ruby 作为一种动态、面向对象且富有表现力的编程语言,提供了丰富的工具和特性来支持代码重构。本文将深入探讨 Ruby 代码重构的常见模式与技巧,并通过实际代码示例展示如何有效地应用它们。
二、提取方法(Extract Method)
- 模式描述 提取方法是一种基本的重构技巧,旨在将一段代码从现有方法中分离出来,形成一个新的独立方法。这样做的好处是提高代码的可读性、可维护性,以及增强代码的复用性。当一个方法执行多个不同的任务,或者一段代码在多个地方重复出现时,提取方法是一个很好的选择。
- 代码示例 假设我们有一个计算订单总价的方法,同时还包含了日志记录的功能:
class Order
def calculate_total
total = 0
items.each do |item|
total += item.price * item.quantity
end
puts "Total price of the order: #{total}"
total
end
end
上述代码中,计算总价和日志记录混在了一起。我们可以将计算总价的部分提取出来:
class Order
def calculate_total
total = calculate_order_total
puts "Total price of the order: #{total}"
total
end
private
def calculate_order_total
total = 0
items.each do |item|
total += item.price * item.quantity
end
total
end
end
- 注意事项
- 在提取方法时,要确保新方法有一个清晰、有意义的名称,能够准确描述其功能。
- 注意处理新方法与原方法之间的参数传递和返回值。如果原方法中的局部变量在新方法中需要使用,可能需要将其作为参数传递给新方法。
三、内联方法(Inline Method)
- 模式描述 与提取方法相反,内联方法是将一个简单的方法调用替换为方法的实际实现。当一个方法的实现过于简单,其存在反而增加了代码的复杂性和阅读成本时,内联方法是合适的重构方式。
- 代码示例 假设有一个简单的方法用于获取订单的折扣率:
class Order
def discount_rate
0.1
end
def discounted_price
original_price * (1 - discount_rate)
end
end
由于 discount_rate
方法非常简单,我们可以将其调用内联到 discounted_price
方法中:
class Order
def discounted_price
original_price * (1 - 0.1)
end
end
- 注意事项
- 内联方法可能会降低代码的可读性,如果原方法名能够清晰表达其功能,且代码阅读者可能不熟悉具体实现,应谨慎使用内联方法。
- 内联方法可能会导致代码重复,如果在多个地方调用了该方法,内联后会使重复代码增加,此时需要权衡利弊。
四、提取变量(Extract Variable)
- 模式描述 提取变量是将一个复杂表达式或重复出现的部分提取为一个变量。这样可以提高代码的可读性,使代码更易于理解和维护。当一个表达式过于冗长或复杂,难以一眼看出其含义时,提取变量是一个有效的重构手段。
- 代码示例 考虑如下计算订单总价并应用折扣和税费的代码:
class Order
def final_price
subtotal = items.map { |item| item.price * item.quantity }.sum
discounted_subtotal = subtotal * (1 - discount_rate)
discounted_subtotal * (1 + tax_rate)
end
end
在这个例子中,我们先提取了订单的小计 subtotal
,然后基于小计计算出折扣后的价格 discounted_subtotal
,使代码逻辑更加清晰。
3. 注意事项
- 变量命名要准确、有意义,能够清晰表达其代表的内容。
- 避免过度提取变量,导致代码中充斥着大量无意义的变量,反而降低了可读性。
五、内联变量(Inline Variable)
- 模式描述 内联变量是将一个只被使用一次且其值很容易理解的变量替换为其实际的值。当变量的存在并没有增加代码的可读性,反而增加了代码的复杂性时,可以考虑内联变量。
- 代码示例 假设有如下代码:
class Order
def price_with_tax
subtotal = items.map { |item| item.price * item.quantity }.sum
tax = subtotal * tax_rate
subtotal + tax
end
end
由于 tax
变量只被使用了一次,且其计算逻辑相对简单,我们可以内联它:
class Order
def price_with_tax
subtotal = items.map { |item| item.price * item.quantity }.sum
subtotal + subtotal * tax_rate
end
end
- 注意事项
- 内联变量时要确保表达式不会变得过于复杂,否则可能会降低代码的可读性。
- 如果该变量在未来可能会被多次使用或者需要进行修改,应谨慎内联。
六、替换条件表达式为多态(Replace Conditional with Polymorphism)
- 模式描述 当代码中存在大量基于条件判断来执行不同行为的逻辑时,代码会变得复杂且难以维护。通过使用多态,可以将条件逻辑分散到不同的子类中,使代码更加清晰和可扩展。
- 代码示例 假设我们有一个根据订单类型计算运费的方法:
class Order
def calculate_shipping
if order_type == :standard
5
elsif order_type == :express
10
else
0
end
end
end
我们可以通过继承和多态来重构这段代码:
class Order
def calculate_shipping
raise NotImplementedError
end
end
class StandardOrder < Order
def calculate_shipping
5
end
end
class ExpressOrder < Order
def calculate_shipping
10
end
end
- 注意事项
- 引入多态可能会增加类的数量,导致代码结构变得复杂,因此需要权衡是否真的有必要。
- 确保各个子类的职责明确,避免子类之间的功能过于相似或出现重复代码。
七、移除临时变量(Remove Temporary Variable)
- 模式描述 临时变量通常在方法内部用于存储中间结果。当临时变量使得代码变得混乱,或者其功能可以通过其他方式(如方法调用链)更好地实现时,可以考虑移除临时变量。
- 代码示例 假设有如下代码用于获取订单中最贵的商品价格:
class Order
def most_expensive_item_price
max_price = 0
items.each do |item|
if item.price > max_price
max_price = item.price
end
end
max_price
end
end
我们可以使用 Ruby 的 max_by
方法来移除临时变量:
class Order
def most_expensive_item_price
items.max_by(&:price)&.price || 0
end
end
- 注意事项
- 在移除临时变量时,要确保新的实现方式不会降低代码的可读性和性能。
- 有些临时变量可能是为了提高性能而存在的,移除时需要进行性能测试。
八、引入参数对象(Introduce Parameter Object)
- 模式描述 当一个方法接受过多的参数时,代码会变得难以阅读和维护。引入参数对象是将相关的参数封装成一个对象,然后将这个对象作为参数传递给方法。
- 代码示例 假设有一个计算订单总价的方法,接受商品价格、数量、折扣率和税率作为参数:
def calculate_total(price, quantity, discount_rate, tax_rate)
subtotal = price * quantity
discounted_subtotal = subtotal * (1 - discount_rate)
discounted_subtotal * (1 + tax_rate)
end
我们可以创建一个 OrderParameters
对象来封装这些参数:
class OrderParameters
attr_reader :price, :quantity, :discount_rate, :tax_rate
def initialize(price, quantity, discount_rate, tax_rate)
@price = price
@quantity = quantity
@discount_rate = discount_rate
@tax_rate = tax_rate
end
end
def calculate_total(params)
subtotal = params.price * params.quantity
discounted_subtotal = subtotal * (1 - params.discount_rate)
discounted_subtotal * (1 + params.tax_rate)
end
- 注意事项
- 参数对象要有明确的职责和合理的命名,能够清晰地表达其所包含参数的意义。
- 避免过度封装,如果参数之间并没有很强的关联性,引入参数对象可能会增加不必要的复杂性。
九、移除控制标志(Remove Control Flag)
- 模式描述 控制标志是在代码中用于控制循环或条件执行的变量。当控制标志使得代码逻辑变得混乱,难以理解时,可以通过重构来移除它。
- 代码示例 假设有如下代码用于查找订单中是否有特定商品:
class Order
def has_specific_item?(item_name)
found = false
items.each do |item|
if item.name == item_name
found = true
break
end
end
found
end
end
我们可以使用 Ruby 的 any?
方法来移除控制标志:
class Order
def has_specific_item?(item_name)
items.any? { |item| item.name == item_name }
end
end
- 注意事项
- 在移除控制标志时,要确保新的实现方式准确地实现了原有的功能。
- 有些情况下,控制标志可能是为了实现复杂的逻辑,移除时需要仔细考虑是否会丢失某些功能。
十、分解条件(Decompose Conditional)
- 模式描述 当一个条件表达式过于复杂,包含多个逻辑判断时,代码会变得难以理解和维护。分解条件是将复杂的条件表达式拆分成多个简单的条件,并分别处理。
- 代码示例 假设有如下代码用于判断订单是否符合促销条件:
class Order
def eligible_for_promotion?
total_amount > 100 && (items.count >= 5 || items.any? { |item| item.category == :electronics })
end
end
我们可以分解这个条件:
class Order
def eligible_for_promotion?
high_amount? && (many_items? || has_electronics?)
end
private
def high_amount?
total_amount > 100
end
def many_items?
items.count >= 5
end
def has_electronics?
items.any? { |item| item.category == :electronics }
end
end
- 注意事项
- 分解后的每个条件方法都要有清晰的命名,能够准确描述其判断的逻辑。
- 确保分解后的条件组合起来能够准确实现原有的复杂条件逻辑。
十一、合并条件表达式(Consolidate Conditional Expression)
- 模式描述 如果在一段代码中,多个条件判断都导致相同的结果,那么可以将这些条件合并为一个条件表达式。这样可以减少重复代码,使逻辑更加清晰。
- 代码示例 假设有如下代码用于处理订单状态:
class Order
def process_order
if order_status == :new
send_confirmation_email
elsif order_status == :pending
send_confirmation_email
end
end
end
我们可以合并条件:
class Order
def process_order
if [:new, :pending].include?(order_status)
send_confirmation_email
end
end
end
- 注意事项
- 合并条件时要确保条件之间的逻辑关系准确,不能遗漏或错误合并条件。
- 如果合并后的条件变得过于复杂,难以理解,应谨慎使用,可能需要寻找其他重构方式。
十二、合并重复的条件片段(Consolidate Duplicate Conditional Fragments)
- 模式描述 当在不同的条件分支中存在重复的代码片段时,可以将这些重复片段提取出来,放在条件判断之前或之后执行。
- 代码示例 假设有如下代码用于根据订单状态执行不同操作,但有重复的日志记录部分:
class Order
def process_order
if order_status == :new
log_order_processing('New order received')
create_order_record
elsif order_status == :pending
log_order_processing('Pending order')
create_order_record
end
end
end
我们可以将重复的 create_order_record
提取出来:
class Order
def process_order
log_order_processing(order_status == :new? 'New order received' : 'Pending order')
create_order_record
end
end
- 注意事项
- 提取重复片段时要确保其执行逻辑在不同条件下都是正确的,不会因为条件变化而产生错误。
- 如果重复片段在不同条件下有细微差别,需要仔细考虑如何重构,可能需要参数化或进一步分解。
十三、以多态取代与算法相关的条件表达式(Replace Conditional with Polymorphism for Algorithm - related Logic)
- 模式描述 在某些情况下,代码中会根据不同的条件选择不同的算法来执行。通过使用多态,可以将这些算法封装到不同的类中,根据条件创建相应的对象并调用其方法,使代码更加灵活和易于扩展。
- 代码示例 假设我们有一个根据订单类型选择不同运费计算算法的代码:
class Order
def calculate_shipping
if order_type == :standard
calculate_standard_shipping
elsif order_type == :express
calculate_express_shipping
end
end
private
def calculate_standard_shipping
# 标准运费计算逻辑
5
end
def calculate_express_shipping
# 快递运费计算逻辑
10
end
end
我们可以通过多态来重构:
class ShippingCalculator
def calculate_shipping
raise NotImplementedError
end
end
class StandardShippingCalculator < ShippingCalculator
def calculate_shipping
5
end
end
class ExpressShippingCalculator < ShippingCalculator
def calculate_shipping
10
end
end
class Order
def calculate_shipping
calculator = case order_type
when :standard
StandardShippingCalculator.new
when :express
ExpressShippingCalculator.new
end
calculator.calculate_shipping
end
end
- 注意事项
- 多态实现可能会增加类的数量和代码的复杂性,要确保在项目规模和维护成本上是可接受的。
- 各个算法类的职责要明确,并且要有良好的文档说明其功能和实现逻辑。
十四、总结
代码重构是一个持续的过程,它有助于提高代码的质量、可维护性和可扩展性。在 Ruby 编程中,通过应用上述常见的重构模式与技巧,可以使代码更加优雅、清晰和高效。在实际项目中,要根据具体情况灵活选择和组合这些重构方式,不断优化代码库,以应对不断变化的需求。同时,在进行重构时,要确保有足够的测试覆盖,避免引入新的 bugs。通过持续的代码重构实践,开发者能够不断提升自己的编程能力,打造出高质量的 Ruby 应用程序。