一、Ruby on Rails 代码重构的常见误区

重构是很多 Rails 开发者都听过的词,简单说就是把代码改得更干净、更好维护,但不少人重构时容易踩坑,反而把项目搞得更乱。

1.1 为了“重构”而重构,忽略业务逻辑

很多人看到别人说某个写法不规范,或者觉得自己的代码看起来不够“高级”,就盲目改,完全没考虑业务的实际需求。比如有个电商项目的订单模块,原来的代码是直接在 Order 模型里写了判断订单状态的逻辑:

# 技术栈:Ruby on Rails 7.0
class Order < ApplicationRecord
  # 订单状态:pending=待付款,paid=已付款,shipped=已发货,completed=已完成
  def is_paid?
    status == 'paid'
  end

  def is_shipped?
    status == 'shipped'
  end

  # 其他状态判断方法
end

有人觉得这种方法太简单,想改成用状态机 gem 来管理,结果改完之后,原来依赖状态判断的支付回调、物流同步接口都出了问题——因为状态机的状态值和原来的字符串不一样,还没做好兼容,导致很多订单状态混乱。其实这个项目的订单状态只有 4 种,业务逻辑也很简单,原来的写法完全够用,根本没必要硬改。

1.2 过度拆分,增加不必要的复杂度

Rails 里经常会提到“瘦模型”,很多人理解成要把模型里的代码拆得越碎越好,结果拆出一堆没用的类。比如原来的 User 模型里有个生成用户专属分享链接的方法:

# 技术栈:Ruby on Rails 7.0
class User < ApplicationRecord
  # 生成用户的专属分享链接,用于邀请好友注册
  def share_link
    "#{Rails.application.config.app_url}/invite?code=#{invite_code}"
  end
end

有人觉得这个方法不属于 User 模型的核心逻辑,就拆出了一个 UserShareLinkService 类:

# 技术栈:Ruby on Rails 7.0
class UserShareLinkService
  def initialize(user)
    @user = user
  end

  def generate
    "#{Rails.application.config.app_url}/invite?code=#{@user.invite_code}"
  end
end

用的时候还要先实例化这个服务类:UserShareLinkService.new(current_user).generate。本来一行代码就能搞定的事,现在多了一个类,调用方式也变复杂了,新人接手的时候还要额外理解这个服务类的作用,完全是没必要的拆分。

1.3 忽略测试,重构后出问题

重构最容易犯的错就是没测就上线。Rails 本身有完善的测试框架,但很多人重构的时候跳过了测试步骤。比如有个博客项目的 Post 模型里有个获取热门文章的方法:

# 技术栈:Ruby on Rails 7.0
class Post < ApplicationRecord
  # 获取最近一周内点赞数超过10的热门文章
  def self.hot_posts
    where('created_at > ?', 1.week.ago).where('likes_count > 10').order(likes_count: :desc)
  end
end

有人觉得这个方法的查询条件太冗余,改成了:

# 技术栈:Ruby on Rails 7.0
class Post < ApplicationRecord
  def self.hot_posts
    where(created_at: 1.week.ago..).where('likes_count > 10').order(likes_count: :desc)
  end
end

改完之后没测,上线后发现热门文章的数量少了很多——原来的 created_at > ? 是排除掉 1 周前的文章,而 Rails 里的范围查询默认是包含边界的,created_at: 1.week.ago.. 会把刚好 1 周前的文章也包含进去?不对,实际问题是改的时候把“最近一周”的逻辑搞反了,应该是 created_at > 1.week.ago,但改成范围的时候写成了 1.week.ago..,导致查询的是 1 周前及之后的所有文章,和原来的逻辑完全相反,这就是没做单元测试导致的问题。

二、Rails 可维护性提升的实践

可维护性好的代码,就是新人接手能快速看懂,改代码不容易出问题,加新功能也很顺畅。下面说几个实际能用的方法。

2.1 合理使用 Rails 自带的特性,减少自定义代码

Rails 本身已经做了很多优化,很多时候不需要自己写复杂的逻辑。比如关联模型的操作,原来的代码可能是这样的:

# 技术栈:Ruby on Rails 7.0
# 控制器里的代码,给用户创建地址
class AddressesController < ApplicationController
  def create
    user = User.find(params[:user_id])
    address = user.addresses.new(address_params)
    if address.save
      redirect_to user_path(user), notice: '地址创建成功'
    else
      render :new
    end
  end

  private

  def address_params
    params.require(:address).permit(:street, :city, :zip_code)
  end
end

其实 Rails 可以用嵌套资源的方式简化,路由配置改成:

# 技术栈:Ruby on Rails 7.0
Rails.application.routes.draw do
  resources :users do
    resources :addresses, only: [:create]
  end
end

然后控制器里的代码可以简化,甚至不需要手动找用户:

# 技术栈:Ruby on Rails 7.0
class AddressesController < ApplicationController
  def create
    address = @user.addresses.new(address_params)
    if address.save
      redirect_to user_path(@user), notice: '地址创建成功'
    else
      render :new
    end
  end

  private

  def address_params
    params.require(:address).permit(:street, :city, :zip_code)
  end

  # 用 Rails 的 before_action 自动加载用户,不需要自己写 find 逻辑
  before_action :load_user
  def load_user
    @user = User.find(params[:user_id])
  end
end

这样代码更简洁,也符合 Rails 的约定,新人一看就知道是嵌套资源的操作。

2.2 善用查询对象,避免模型里的复杂查询

模型里经常会有很多复杂的查询逻辑,比如筛选、排序,这些逻辑如果都写在模型里,模型会变得很臃肿。这时候可以用查询对象来拆分。比如电商项目里,要筛选符合条件的订单,原来的代码是:

# 技术栈:Ruby on Rails 7.0
class Order < ApplicationRecord
  # 筛选指定时间范围、指定状态、指定用户的订单
  def self.filter(start_date:, end_date:, status:, user_id:)
    scope = all
    scope = scope.where('created_at >= ?', start_date) if start_date
    scope = scope.where('created_at <= ?', end_date) if end_date
    scope = scope.where(status: status) if status
    scope = scope.where(user_id: user_id) if user_id
    scope.order(created_at: :desc)
  end
end

如果筛选条件越来越多,这个方法会变得越来越长,模型也会越来越乱。这时候可以拆成查询对象:

# 技术栈:Ruby on Rails 7.0
# 先在 app/queries 目录下创建 OrderFilterQuery 类
class OrderFilterQuery
  attr_reader :scope

  def initialize(scope = Order.all)
    @scope = scope
  end

  # 筛选时间范围
  def by_date_range(start_date, end_date)
    scope = @scope
    scope = scope.where('created_at >= ?', start_date) if start_date
    scope = scope.where('created_at <= ?', end_date) if end_date
    @scope = scope
    self # 返回自身,方便链式调用
  end

  # 筛选订单状态
  def by_status(status)
    @scope = @scope.where(status: status) if status
    self
  end

  # 筛选用户
  def by_user(user_id)
    @scope = @scope.where(user_id: user_id) if user_id
    self
  end

  # 执行查询,返回结果
  def call
    @scope.order(created_at: :desc)
  end
end

用的时候就很清晰:

# 技术栈:Ruby on Rails 7.0
# 控制器里调用
orders = OrderFilterQuery.new.by_date_range(params[:start_date], params[:end_date])
                               .by_status(params[:status])
                               .by_user(params[:user_id])
                               .call

这样模型里的复杂查询逻辑被拆出来了,每个筛选条件都是独立的方法,改起来也方便,新人一看就知道每个方法是做什么的。

2.3 写好测试,保障重构的安全性

重构前一定要确保有完善的测试,特别是单元测试。比如前面提到的热门文章方法,改之前先写测试:

# 技术栈:Ruby on Rails 7.0
# 测试文件:test/models/post_test.rb
require "test_helper"

class PostTest < ActiveSupport::TestCase
  test "hot_posts 返回最近一周内点赞数超过10的文章" do
    # 创建测试数据:最近一周内的热门文章
    hot_post1 = Post.create!(created_at: 2.days.ago, likes_count: 15)
    hot_post2 = Post.create!(created_at: 3.days.ago, likes_count: 20)
    # 创建测试数据:不符合条件的文章
    old_post = Post.create!(created_at: 2.weeks.ago, likes_count: 20)
    low_like_post = Post.create!(created_at: 1.day.ago, likes_count: 5)

    result = Post.hot_posts

    # 验证结果:只包含两个热门文章,且按点赞数降序排列
    assert_equal [hot_post2, hot_post1], result
  end
end

改完代码之后再跑测试,如果测试不通过,就能立刻发现问题,不会上线后才出故障。

三、实践的应用场景与注意事项

这些提升可维护性的方法,不是所有项目都适用,要根据实际情况来选。比如查询对象,适合项目里有大量复杂筛选逻辑的情况,比如电商的订单管理、后台的用户管理;如果项目很小,筛选逻辑很简单,就没必要拆查询对象,反而会增加复杂度。

用这些方法的时候,还要注意几个点:第一,不要为了符合规范而写代码,一切以业务需求为核心;第二,重构的时候要小步迭代,改一点测一点,不要一次性改很多;第三,要考虑团队的技术水平,比如如果团队里很多人对查询对象不熟悉,就不要强行用,还是用大家都能看懂的写法。

四、总结

Rails 代码重构不是越复杂越好,也不是越简洁越好,核心是让代码更符合业务需求,更方便团队维护。常见的误区就是盲目重构、过度拆分、忽略测试,而提升可维护性的实践,就是合理用 Rails 自带的特性、善用查询对象、写好测试。不管用什么方法,都要记住,代码是给人看的,其次才是给机器运行的,好的代码应该是新人接手能快速上手,改代码不容易出问题,这样才能让项目长期稳定运行。