Rails 编码风格指南

好的风格是成功的一半。


更新记录

  • 2016.06.16: 初稿

网上已经有一份非常详尽的 Rails 风格指南,但是光看是没办法快速入门的,而且前面提到的指南太详尽,有的时候反而不太实用,所以我决定一边学习一边把自己的理解记录下来(几乎全部内容都来自前面的链接,我只是根据自己习惯重新编排了一下),一份良好的风格指南是极佳的学习资料。注:本文中的一些建议只适用于 Rails 4.0+ 版本

配置

  • 自定义的初始化代码应放在 config/initializers 目录下。 Initializers 目录中的代码在应用启动时被执行
  • 每个 gem 的初始化代码应放在单独的文件中,并且文件名应与 gem 的名称相同。例如: carrierwave.rb, active_admin.rb
  • 将所有环境下都通用的配置放在 config/application.rb 文件中
  • 创建一个与生产环境高度相似的 staging 环境
  • 其它配置应保存在 YAML 文件中,存放在 config/ 目录下
  • 从 Rails 4.2 开始,可以通过 config_for 这个新方法轻松地加载 YAML 配置文件 Rails::Application.config_for(:yaml_file)

控制器

  • 控制器应该保持苗条 ― 它们应该只为视图层提供数据,不应包含任何业务逻辑(所有业务逻辑都应当放在模型里)
  • 每个控制器的动作(理论上)应当只调用一个除了初始的 find 或 new 之外的方法
  • 控制器与视图之间共享不超过两个实例变量

模型

  • 自由地引入不是 ActiveRecord 的模型类
  • 模型的命名应有意义(但简短)且不含缩写

如果需要模型类有与 ActiveRecord 类似的行为(如验证),但又不想有 ActiveRecord 的数据库功能,应使用 ActiveAttr 这个 gem

class Message
include ActiveAttr::Model
attribute :name
attribute :email
attribute :content
attribute :priority
attr_accessible :name, :email, :content
validates_presence_of :name
validates_format_of :email, :with => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i
validates_length_of :content, :maximum => 500
end

ActiveRecord

避免改动缺省的 ActiveRecord 惯例(表的名字、主键等),除非你有一个充分的理由(比如,不受你控制的数据库)

# 差 - 如果你能更改数据库的 schema,那就不要这样写
class Transaction < ActiveRecord::Base
self.table_name = 'order'
...
end

把宏风格的方法调用(has_many, validates 等)放在类定义语句的最前面

class User < ActiveRecord::Base
# 默认的 scope 放在最前(如果有的话)
default_scope { where(active: true) }
# 接下来是常量初始化
COLORS = %w(red green blue)
# 然后是 attr 相关的宏
attr_accessor :formatted_date_of_birth
attr_accessible :login, :first_name, :last_name, :email, :password
# 紧接着是与关联有关的宏
belongs_to :country
has_many :authentications, dependent: :destroy
# 以及与验证有关的宏
validates :email, presence: true
validates :username, presence: true
validates :username, uniqueness: { case_sensitive: false }
validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ }
validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true}
# 下面是回调方法
before_save :cook
before_save :update_username_lower
# 其它的宏(如 devise)应放在回调方法之后
...
end

has_many :through 优于 has_and_belongs_to_many。 使用 has_many :through 允许 join 模型有附加的属性及验证

# 不太好 - 使用 has_and_belongs_to_many
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
end
class Group < ActiveRecord::Base
has_and_belongs_to_many :users
end
# 更好 - 使用 has_many :through
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
end

self[:attribute]read_attribute(:attribute) 更好

# 差
def amount
read_attribute(:amount) * 100
end
# 好
def amount
self[:amount] * 100
end

self[:attribute] = value 优于 write_attribute(:attribute, value)

# 差
def amount
write_attribute(:amount, 100)
end
# 好
def amount
self[:amount] = 100
end

总是使用新式的 “sexy” 验证

# 差
validates_presence_of :email
validates_length_of :email, maximum: 100
# 好
validates :email, presence: true, length: { maximum: 100 }

当一个自定义的验证规则使用次数超过一次时,或该验证规则是基于正则表达式时,应该创建一个自定义的验证规则文件

# 差
class Person
validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
end
# 好
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
end
end
class Person
validates :email, email: true
end

ActiveRecord 查询

不要在查询中使用字符串插值,它会使你的代码有被 SQL 注入攻击的风险

# 差——插值的参数不会被转义
Client.where("orders_count = #{params[:orders]}")
# 好——参数会被适当转义
Client.where('orders_count = ?', params[:orders])

当查询中有超过 1 个占位符时,应考虑使用名称占位符,而非位置占位符

# 一般般
Client.where(
'created_at >= ? AND created_at <= ?',
params[:start_date], params[:end_date]
)
# 好
Client.where(
'created_at >= :start_date AND created_at <= :end_date',
start_date: params[:start_date], end_date: params[:end_date]
)

当只需要通过 id 查询单个记录时,优先使用 find 而不是 where

# 差
User.where(id: id).take
# 好
User.find(id)

当只需要通过属性查询单个记录时,优先使用 find_by 而不是 where

# 差
User.where(first_name: 'Bruce', last_name: 'Wayne').first
# 好
User.find_by(first_name: 'Bruce', last_name: 'Wayne')

当需要处理多条记录时,应使用 find_each

# 差——一次性加载所有记录
# 当 users 表有成千上万条记录时,非常低效
User.all.each do |user|
NewsMailer.weekly(user).deliver_now
end
# 好——分批检索记录
User.find_each do |user|
NewsMailer.weekly(user).deliver_now
end

试图

  • 不要直接从视图调用模型层
  • 复杂的格式化不应放在视图中,而应提取为视图 helper 或模型中的方法
  • 应使用 partial 模版与布局来减少代码重复

Assets

应使用 assets pipeline 来管理应用的资源结构。

  • 自定义的样式表、JavaScript 文件或图片文件,应放在 app/assets 目录下。
  • 把自己开发但不好归类的库文件,应放在 lib/assets/ 目录下。
  • 第三方代码,如 jQuery 或 bootstrap,应放在 vendor/assets 目录下。
  • 尽可能使用资源的 gem 版。例如: jquery-rails, jquery-ui-rails, bootstrap-sass, zurb-foundation

Time

application.rb 里设置相应的时区

config.time_zone = 'Eastern European Time'
# 可选配置——注意取值只能是 :utc 或 :local 中的一个(默认为 :utc)
config.active_record.default_timezone = :local

不要使用 Time.parse

# 差
Time.parse('2015-03-02 19:05:37') # => 会假设时间是基于操作系统的时区。
# 好
Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00

不要使用 Time.now

# 差
Time.now # => 无视所配置的时区,返回操作系统时间。
# 好
Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00
Time.current # 结果同上,但更简洁
捧个钱场?