MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Ruby 的代码重构技巧

2021-01-093.6k 阅读

一、引言

在软件开发的旅程中,代码就如同我们搭建的一座大厦,随着时间推移和功能的不断叠加,最初简洁有序的结构可能逐渐变得错综复杂。代码重构,作为提升代码质量、增强可维护性和扩展性的关键手段,在编程实践中占据着举足轻重的地位。Ruby,作为一门优雅且富有表现力的编程语言,拥有一套独特的代码重构技巧。接下来,让我们深入探索Ruby的代码重构世界,学习如何让我们的Ruby代码焕发出新的活力。

二、提炼方法(Extract Method)

2.1 概念与作用

在Ruby代码中,常常会遇到一些方法内部包含了过多的逻辑,导致代码冗长且难以理解。提炼方法这一重构技巧,就是将这些复杂逻辑中的一部分提取出来,封装成一个新的独立方法。这样做不仅能使原方法的逻辑更加清晰,还能提高代码的复用性。

2.2 示例

假设我们有一个处理订单的方法,用于计算订单的总金额并打印订单信息:

def process_order(items)
  total_price = 0
  items.each do |item|
    total_price += item.price * item.quantity
  end
  tax = total_price * 0.1
  total_price_with_tax = total_price + tax
  puts "Order items: #{items.map(&:name).join(', ')}"
  puts "Total price before tax: #{total_price}"
  puts "Tax: #{tax}"
  puts "Total price with tax: #{total_price_with_tax}"
end

这段代码虽然功能完整,但逻辑较为混乱。我们可以通过提炼方法来优化它:

def calculate_total_price(items)
  items.inject(0) do |sum, item|
    sum + item.price * item.quantity
  end
end

def calculate_tax(total_price)
  total_price * 0.1
end

def print_order_info(items, total_price, tax, total_price_with_tax)
  puts "Order items: #{items.map(&:name).join(', ')}"
  puts "Total price before tax: #{total_price}"
  puts "Tax: #{tax}"
  puts "Total price with tax: #{total_price_with_tax}"
end

def process_order(items)
  total_price = calculate_total_price(items)
  tax = calculate_tax(total_price)
  total_price_with_tax = total_price + tax
  print_order_info(items, total_price, tax, total_price_with_tax)
end

通过提炼方法,process_order方法变得更加简洁明了,每个提炼出来的方法都专注于单一的职责,提高了代码的可读性和维护性。

三、合并重复代码(Consolidate Duplicate Code)

3.1 识别重复代码

在大型Ruby项目中,重复代码是一个常见的问题。它可能出现在不同的方法、类甚至模块中。重复代码不仅增加了代码量,还使得维护变得困难,因为一处修改可能需要在多个地方同步进行。我们需要仔细审查代码,寻找那些相似的代码块。

3.2 示例

假设有两个类,BookMagazine,都有计算价格的方法,且部分逻辑重复:

class Book
  def initialize(title, author, price)
    @title = title
    @author = author
    @price = price
  end

  def calculate_discounted_price(discount_rate)
    if discount_rate >= 0 && discount_rate <= 1
      discounted_price = @price * (1 - discount_rate)
      puts "#{@title} by #{@author} discounted price: #{discounted_price}"
      discounted_price
    else
      puts "Invalid discount rate"
      nil
    end
  end
end

class Magazine
  def initialize(title, issue_date, price)
    @title = title
    @issue_date = issue_date
    @price = price
  end

  def calculate_discounted_price(discount_rate)
    if discount_rate >= 0 && discount_rate <= 1
      discounted_price = @price * (1 - discount_rate)
      puts "#{@title} issue #{@issue_date} discounted price: #{discounted_price}"
      discounted_price
    else
      puts "Invalid discount rate"
      nil
    end
  end
end

可以看到,calculate_discounted_price方法中的核心逻辑是重复的。我们可以通过创建一个基类来合并这些重复代码:

class Publication
  def initialize(price)
    @price = price
  end

  def calculate_discounted_price(discount_rate)
    if discount_rate >= 0 && discount_rate <= 1
      discounted_price = @price * (1 - discount_rate)
      puts "#{display_info} discounted price: #{discounted_price}"
      discounted_price
    else
      puts "Invalid discount rate"
      nil
    end
  end

  def display_info
    raise NotImplementedError
  end
end

class Book < Publication
  def initialize(title, author, price)
    super(price)
    @title = title
    @author = author
  end

  def display_info
    "#{@title} by #{@author}"
  end
end

class Magazine < Publication
  def initialize(title, issue_date, price)
    super(price)
    @title = title
    @issue_date = issue_date
  end

  def display_info
    "#{@title} issue #{@issue_date}"
  end
end

这样,重复的计算折扣价格的逻辑被抽取到了基类Publication中,BookMagazine类只需要专注于自身特有的信息展示,代码得到了优化。

四、移除临时变量(Remove Temporary Variable)

4.1 临时变量的问题

临时变量在代码中用于临时存储中间结果,但过多的临时变量会使代码的逻辑变得模糊,增加阅读和维护的难度。在Ruby中,我们可以通过一些技巧来移除不必要的临时变量。

4.2 示例

考虑以下计算数组平方和的代码:

def sum_of_squares(numbers)
  squares = []
  numbers.each do |number|
    squares << number ** 2
  end
  sum = squares.inject(0) do |acc, square|
    acc + square
  end
  sum
end

这里使用了 squaressum两个临时变量。我们可以通过链式调用方法来移除这些临时变量:

def sum_of_squares(numbers)
  numbers.inject(0) do |acc, number|
    acc + number ** 2
  end
end

通过这种方式,代码更加简洁,逻辑也更加直接,避免了临时变量带来的复杂性。

五、以多态取代条件表达式(Replace Conditional with Polymorphism)

5.1 条件表达式的弊端

在Ruby代码中,复杂的条件表达式会使代码难以理解和维护。尤其是当条件逻辑与对象的类型相关时,使用多态可以使代码更加优雅和可扩展。

5.2 示例

假设我们有一个根据不同图形类型计算面积的方法:

def calculate_area(shape)
  if shape.type == :circle
    Math::PI * shape.radius ** 2
  elsif shape.type == :rectangle
    shape.length * shape.width
  elsif shape.type == :triangle
    0.5 * shape.base * shape.height
  else
    raise "Unsupported shape type"
  end
end

这种条件表达式随着图形类型的增加会变得越来越复杂。我们可以通过多态来优化:

class Shape
  def calculate_area
    raise NotImplementedError
  end
end

class Circle < Shape
  def initialize(radius)
    @radius = radius
  end

  def calculate_area
    Math::PI * @radius ** 2
  end
end

class Rectangle < Shape
  def initialize(length, width)
    @length = length
    @width = width
  end

  def calculate_area
    @length * @width
  end
end

class Triangle < Shape
  def initialize(base, height)
    @base = base
    @height = height
  end

  def calculate_area
    0.5 * @base * @height
  end
end

def calculate_area(shape)
  shape.calculate_area
end

通过多态,代码变得更加清晰,每个图形类负责自己的面积计算逻辑,增加新的图形类型也更加容易,只需要创建一个新的类并实现calculate_area方法即可。

六、简化条件表达式(Simplify Conditional Expression)

6.1 复杂条件的问题

复杂的条件表达式,如嵌套的if - else语句或多个条件的组合,会使代码的逻辑变得晦涩难懂。在Ruby中,我们可以运用一些技巧来简化它们。

6.2 示例

假设有这样一个复杂的条件判断:

def is_eligible_for_discount(user)
  if user.age >= 60 && (user.purchase_count >= 10 || user.membership_type == :premium)
    true
  elsif user.age < 18 && user.purchase_count >= 5
    true
  else
    false
  end
end

我们可以通过将部分条件提取成方法来简化:

def senior_user_eligible?(user)
  user.age >= 60 && (user.purchase_count >= 10 || user.membership_type == :premium)
end

def young_user_eligible?(user)
  user.age < 18 && user.purchase_count >= 5
end

def is_eligible_for_discount(user)
  senior_user_eligible?(user) || young_user_eligible?(user)
end

这样,每个方法专注于一个特定的条件逻辑,整体的条件判断变得更加清晰。

七、引入解释性变量(Introduce Explaining Variable)

7.1 作用

在Ruby代码中,有时会遇到一些复杂的表达式,其含义并不直观。引入解释性变量可以使这些表达式的意图更加清晰,提高代码的可读性。

7.2 示例

考虑以下计算订单最终价格的代码:

def calculate_final_price(order)
  base_price = order.items.inject(0) { |sum, item| sum + item.price * item.quantity }
  discount = base_price > 100? base_price * 0.1 : 0
  shipping_fee = order.distance > 50? 10 : 0
  base_price - discount + shipping_fee
end

通过引入base_pricediscountshipping_fee这些解释性变量,我们可以清楚地看到每个部分在计算最终价格中的作用,代码的可读性得到了显著提升。

八、封装字段(Encapsulate Field)

8.1 概念

在Ruby类中,直接暴露实例变量可能会导致外部代码随意修改对象的状态,破坏对象的封装性。封装字段就是通过访问器方法(getter和setter)来控制对实例变量的访问。

8.2 示例

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  def name
    @name
  end

  def age
    @age
  end

  def age=(new_age)
    if new_age >= 0
      @age = new_age
    else
      raise "Invalid age"
    end
  end
end

通过定义nameage的访问器方法,我们可以在设置age时进行合法性检查,保护了对象的状态,同时外部代码也只能通过这些方法来访问和修改实例变量,增强了代码的安全性和可维护性。

九、重构与测试

9.1 测试的重要性

在进行代码重构时,测试是至关重要的保障。它可以确保我们在重构代码的过程中,不会破坏原有的功能。在Ruby中,我们可以使用诸如RSpec或MiniTest这样的测试框架。

9.2 示例

以之前的Book类为例,我们可以使用RSpec来编写测试:

require 'rspec'
require_relative 'book'

describe Book do
  let(:book) { Book.new('Ruby Programming', 'David Thomas', 20) }

  describe '#calculate_discounted_price' do
    it 'calculates discounted price correctly' do
      expect(book.calculate_discounted_price(0.1)).to eq(18)
    end

    it 'handles invalid discount rate' do
      expect(book.calculate_discounted_price(1.5)).to be_nil
    end
  end
end

在重构Book类的calculate_discounted_price方法时,我们可以运行这些测试,确保重构后的代码仍然满足预期的功能。

十、持续重构

10.1 重构不是一次性任务

代码重构不是在项目开发完成后才进行的一次性工作,而是贯穿整个软件开发周期的持续过程。随着项目的发展,新功能的添加和需求的变化,代码可能会逐渐变得复杂,这时就需要适时地进行重构。

10.2 示例

假设我们的订单处理系统最初只有简单的商品订单处理功能。随着业务的拓展,需要支持团购订单、预售订单等新的订单类型。在添加这些新功能的过程中,原有的订单处理代码可能会变得臃肿和混乱。我们就需要持续地运用重构技巧,如提炼方法、以多态取代条件表达式等,来保持代码的清晰和可维护性。

通过不断地重构,我们的Ruby代码将始终保持良好的结构,易于理解、维护和扩展,为项目的长期发展奠定坚实的基础。在实际的开发工作中,我们要养成持续重构的习惯,让代码在成长的过程中始终保持健康和高效。