一、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 自带的特性、善用查询对象、写好测试。不管用什么方法,都要记住,代码是给人看的,其次才是给机器运行的,好的代码应该是新人接手能快速上手,改代码不容易出问题,这样才能让项目长期稳定运行。
评论
围绕“Ruby on Rails 代码重构的常见误区与可维护性提升实践”参与讨论