Ruby 风格指南
这是 GitLab 特定的 Ruby 代码风格指南。本页面记录的所有内容都可以 重新讨论。
我们使用 RuboCop 来执行 Ruby 风格指南规则。
当缺少 RuboCop 规则时,请参考以下风格指南作为编写惯用 Ruby 的通用指导原则:
通常,如果某种风格未被现有的 RuboCop 规则或上述风格指南涵盖,那么它不应该成为阻碍。
我们已经决定对某些风格 不持有强烈意见。
另请参阅:
我们没有规则的风格
这些风格没有 RuboCop 规则支持。
对于添加到本部分的每种风格,请从该部分的 历史记录 中链接讨论,以提供上下文并作为参考。
使用 attr_reader 访问实例变量
在类中,可以通过多种方式访问实例变量:
# public
class Foo
attr_reader :my_var
def initialize(my_var)
@my_var = my_var
end
def do_stuff
puts my_var
end
end
# private
class Foo
def initialize(my_var)
@my_var = my_var
end
private
attr_reader :my_var
def do_stuff
puts my_var
end
end
# direct
class Foo
def initialize(my_var)
@my_var = my_var
end
private
def do_stuff
puts @my_var
end
end公共属性应仅在类外部访问时使用。 对于仅在内部访问的属性,使用哪种策略没有强烈意见,只要相关代码保持一致即可。
换行风格指南
除了 RuboCop 的 Layout/EmptyLinesAroundMethodBody 和 Cop/LineBreakAroundConditionalBlock 强制执行某些换行风格外,我们还有以下没有 RuboCop 支持的指导原则。
规则:仅用换行符分隔代码以将相关逻辑分组在一起
# bad
def method
issue = Issue.new
issue.save
render json: issue
end# good
def method
issue = Issue.new
issue.save
render json: issue
end规则:代码块前换行
# bad
def method
issue = Issue.new
if issue.save
render json: issue
end
end# good
def method
issue = Issue.new
if issue.save
render json: issue
end
end例外:当代码块开始或结束于另一个代码块内部时,无需换行
# bad
def method
if issue
if issue.valid?
issue.save
end
end
end# good
def method
if issue
if issue.valid?
issue.save
end
end
endRails / ActiveRecord
本节包含 Rails 和 ActiveRecord 使用的 GitLab 特定指导原则。
避免 ActiveRecord 回调
ActiveRecord 回调 允许 “在对象状态改变之前或之后触发逻辑。”
当没有更好的替代方案时才使用回调,但只有在您 完全理解这样做的原因时才使用。
当为 ActiveRecord 对象添加新的生命周期事件时,最好 将逻辑添加到服务类而不是回调中。
为什么要避免回调
通常,应该避免回调,因为:
- 回调难以推理,因为调用顺序不明显且 它们破坏了代码的叙述性。
- 回调更难定位和导航,因为它们依赖反射来 触发而不是普通的方法调用。
- 回调难以选择性地应用于对象的状态, 因为更改总是触发整个回调链。
- 回调将逻辑困在 ActiveRecord 类中。这种紧密耦合鼓励 胖模型,包含过多的业务逻辑,而这些逻辑可以存在于 服务对象中,这些对象更可重用、可组合,并且更容易测试。
- 对象的非法状态转换可以通过 属性验证更好地强制执行。
- 大量使用回调会影响工厂创建速度。对于某些类 拥有数百个回调,为自动化测试创建该对象的实例 可能是一个非常缓慢的操作,导致测试变慢。
其中一些示例在 thoughtbot 的这个视频 中进行了讨论。
GitLab 代码库严重依赖回调,一旦添加就很难重构, 因为存在不可见的依赖关系。因此,本指导原则不要求 删除所有现有回调。
何时使用回调
回调可以在特殊情况下使用。添加回调 有意义的示例:
- 依赖项使用回调,我们想要覆盖回调 行为。
- 增加缓存计数。
- 仅与当前模型数据相关的数据规范化。
从回调迁移到服务的示例
有一个项目,具有以下基本数据模型:
class Project
has_one :repository
end
class Repository
belongs_to :project
end假设我们想在项目创建后创建一个仓库,并使用 项目名称作为仓库名称。熟悉 Rails 的开发者可能会立即想到: 这听起来像是 ActiveRecord 回调的工作!然后添加这段代码:
class Project
has_one :repository
after_initialize :create_random_name
after_create :create_repository
def create_random_name
SecureRandom.alphanumeric
end
def create_repository
Repository.create!(project: self)
end
end
class Repository
after_initialize :set_name
def set_name
name = project.name
end
end
class ProjectsController
def create
Project.create! # 同时创建仓库并命名
end
end虽然这对于一个小型 Rails 应用来说似乎相当无害,但一旦您的 Rails 应用变得庞大和复杂,通过回调添加这种逻辑有很多缺点(所有缺点都列在本文档中)。相反,我们可以将此逻辑添加到服务类中:
class Project
has_one :repository
end
class Repository
belongs_to :project
end
class ProjectCreator
def self.execute
ApplicationRecord.transaction do
name = SecureRandom.alphanumeric
project = Project.create!(name: name)
Repository.create!(project: project, name: name)
end
end
end
class ProjectsController
def create
ProjectCreator.execute
end
end对于如此简单的应用程序,很难看出第二种方法的好处。但我们已经获得了一些好处:
- 可以独立于
Project创建逻辑测试Repository创建逻辑。代码 不再违反迪米特法则(Repository类不需要知道project.name)。 - 调用顺序清晰。
- 对变更开放:如果我们决定在某些情况下不希望为项目创建仓库,我们可以创建一个新的服务类,而不是需要重构
Project和Repository类。 - 每个
Project工厂实例不会创建第二个(Repository)对象。
ApplicationRecord / ActiveRecord 模型作用域
创建新作用域时,请考虑以下前缀。
for_
用于过滤 where(belongs_to: record) 的作用域。
例如:
scope :for_project, ->(project) { where(project: project) }
Timelogs.for_project(project)with_
用于 joins、includes 或过滤 where(has_one: record) 或 where(has_many: record) 或 where(boolean condition) 的作用域
例如:
scope :with_labels, -> { includes(:labels) }
AbuseReport.with_labels
scope :with_status, ->(status) { where(status: status) }
Clusters::AgentToken.with_status(:active)
scope :with_due_date, -> { where.not(due_date: nil) }
Issue.with_due_date也可以使用自定义作用域名称,例如:
scope :undeleted, -> { where('policy_index >= 0') }
Security::Policy.undeletedorder_by_
用于 order 的作用域。
例如:
scope :order_by_name, -> { order(:name) }
Namespace.order_by_name
scope :order_by_updated_at, ->(direction = :asc) { order(updated_at: direction) }
Project.order_by_updated_at(:desc)我们没有意见的风格
如果提出了 RuboCop 规而我们选择不添加,我们应该在本指南中记录该决定,使其更容易被发现,并链接相关讨论作为参考。
字符串字面量的引号
由于修正的工作量巨大,我们不关心字符串 字面量是单引号还是双引号。
之前的讨论包括:
- https://gitlab.com/gitlab-org/gitlab-foss/-/issues/44234
- https://gitlab.com/gitlab-org/gitlab-foss/-/issues/36076
- https://gitlab.com/gitlab-org/gitlab/-/issues/198046
个别小组可能会 选择持有意见 关于其拥有的 有界上下文 中引号风格的一致性,但这些决定仅适用于该上下文中的代码。
类型安全
既然我们已经升级到 Ruby 3,我们有了更多选项来强制执行 类型安全。
其中一些选项作为 Ruby 语法的一部分得到支持,不需要使用特定的类型安全工具,如 Sorbet 或 RBS。然而,我们将来也可能考虑这些工具。
目前,我们可以使用 YARD 注解 来定义类型。 像 RubyMine 这样的 IDE 在显示基于类型的检查错误时提供对 YARD 的支持。
更多信息,请参阅 remote_development 域 README 中的 类型安全。
函数式模式
尽管 Ruby 特别是 Rails 主要基于 面向对象编程 模式,但 Ruby 是一种非常灵活的语言,也支持 函数式编程 模式。
函数式编程模式,特别是在领域逻辑中,通常可以产生更易读、可维护和抗 bug 的代码,同时仍然使用惯用的和熟悉的 Ruby 模式。
然而,函数式编程模式应该谨慎使用,因为某些模式会引起混淆,即使它们直接受到 Ruby 的支持,也应该避免。curry 方法 就是一个可能的例子。
更多信息,请参阅: