Help us learn about your current experience with the documentation. Take the survey.

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/EmptyLinesAroundMethodBodyCop/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
end

Rails / 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)。
  • 调用顺序清晰。
  • 对变更开放:如果我们决定在某些情况下不希望为项目创建仓库,我们可以创建一个新的服务类,而不是需要重构 ProjectRepository 类。
  • 每个 Project 工厂实例不会创建第二个(Repository)对象。

ApplicationRecord / ActiveRecord 模型作用域

创建新作用域时,请考虑以下前缀。

for_

用于过滤 where(belongs_to: record) 的作用域。 例如:

scope :for_project, ->(project) { where(project: project) }
Timelogs.for_project(project)

with_

用于 joinsincludes 或过滤 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.undeleted

order_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 规而我们选择不添加,我们应该在本指南中记录该决定,使其更容易被发现,并链接相关讨论作为参考。

字符串字面量的引号

由于修正的工作量巨大,我们不关心字符串 字面量是单引号还是双引号。

之前的讨论包括:

个别小组可能会 选择持有意见 关于其拥有的 有界上下文 中引号风格的一致性,但这些决定仅适用于该上下文中的代码。

类型安全

既然我们已经升级到 Ruby 3,我们有了更多选项来强制执行 类型安全

其中一些选项作为 Ruby 语法的一部分得到支持,不需要使用特定的类型安全工具,如 SorbetRBS。然而,我们将来也可能考虑这些工具。

目前,我们可以使用 YARD 注解 来定义类型。 像 RubyMine 这样的 IDE 在显示基于类型的检查错误时提供对 YARD 的支持。

更多信息,请参阅 remote_development 域 README 中的 类型安全

函数式模式

尽管 Ruby 特别是 Rails 主要基于 面向对象编程 模式,但 Ruby 是一种非常灵活的语言,也支持 函数式编程 模式。

函数式编程模式,特别是在领域逻辑中,通常可以产生更易读、可维护和抗 bug 的代码,同时仍然使用惯用的和熟悉的 Ruby 模式。 然而,函数式编程模式应该谨慎使用,因为某些模式会引起混淆,即使它们直接受到 Ruby 的支持,也应该避免。curry 方法 就是一个可能的例子。

更多信息,请参阅: