ruby-on-rails-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Ruby on Rails Best Practices

Ruby on Rails 最佳实践

Architecture patterns and coding conventions extracted from Basecamp's production Rails applications (Fizzy and Campfire). Contains 16 rules across 6 categories focused on code organization, maintainability, and following "The Rails Way" with Basecamp's refinements.
本内容提取自Basecamp的生产级Rails应用(Fizzy和Campfire)中的架构模式与编码规范。包含6个分类下的16条规则,聚焦于代码组织、可维护性,以及结合Basecamp改进方案的「Rails之道」实践。

When to Apply

适用场景

Reference these guidelines when:
  • Organizing models, concerns, and controllers
  • Writing background jobs
  • Implementing real-time features with Turbo Streams
  • Deciding where code should live
  • Writing tests for Rails applications
  • Reviewing Rails code for architectural consistency
在以下场景中参考本指南:
  • 组织模型、关注点和控制器
  • 编写后台任务
  • 使用Turbo Streams实现实时功能
  • 确定代码的存放位置
  • 为Rails应用编写测试
  • 评审Rails代码的架构一致性

Rules Summary

规则摘要

Model Organization (HIGH)

模型组织(高优先级)

model-scoped-concerns - @rules/model-scoped-concerns.md

model-scoped-concerns - @rules/model-scoped-concerns.md

Place model-specific concerns in
app/models/model_name/
not
app/models/concerns/
.
ruby
undefined
将模型专属的关注点放置在
app/models/model_name/
目录下,而非
app/models/concerns/
ruby
undefined

Directory structure

目录结构

app/models/ ├── card.rb ├── card/ │ ├── closeable.rb # Card::Closeable │ ├── searchable.rb # Card::Searchable │ └── assignable.rb # Card::Assignable
app/models/ ├── card.rb ├── card/ │ ├── closeable.rb # Card::Closeable │ ├── searchable.rb # Card::Searchable │ └── assignable.rb # Card::Assignable

app/models/card.rb

app/models/card.rb

class Card < ApplicationRecord include Closeable, Searchable, Assignable

Ruby resolves from Card:: namespace first

end
undefined
class Card < ApplicationRecord include Closeable, Searchable, Assignable

Ruby会优先从Card::命名空间解析

end
undefined

concern-naming - @rules/concern-naming.md

concern-naming - @rules/concern-naming.md

Use
-able
suffix for behavior concerns, nouns for feature concerns.
ruby
undefined
行为类关注点使用
-able
后缀,功能类关注点使用名词。
ruby
undefined

Behaviors: -able suffix

行为类:使用-able后缀

module Card::Closeable # Can be closed module Card::Searchable # Can be searched module User::Mentionable # Can be mentioned
module Card::Closeable # 可关闭 module Card::Searchable # 可搜索 module User::Mentionable # 可被提及

Features: nouns

功能类:使用名词

module User::Avatar # Has avatar module User::Role # Has role module Card::Mentions # Has @mentions
undefined
module User::Avatar # 包含头像功能 module User::Role # 包含角色功能 module Card::Mentions # 包含@提及功能
undefined

template-method-concerns - @rules/template-method-concerns.md

template-method-concerns - @rules/template-method-concerns.md

Use template methods in shared concerns for customizable behavior.
ruby
undefined
在共享关注点中使用模板方法实现可定制化行为。
ruby
undefined

app/models/concerns/searchable.rb (shared)

app/models/concerns/searchable.rb (共享)

module Searchable def search_title raise NotImplementedError end end
module Searchable def search_title raise NotImplementedError end end

app/models/card/searchable.rb (model-specific)

app/models/card/searchable.rb (模型专属)

module Card::Searchable include ::Searchable
def search_title title # Implement the hook end end
undefined
module Card::Searchable include ::Searchable
def search_title title # 实现钩子方法 end end
undefined

Background Jobs (HIGH)

后台任务(高优先级)

paired-async-methods - @rules/paired-async-methods.md

paired-async-methods - @rules/paired-async-methods.md

Pair sync methods with
_later
variants that enqueue jobs.
ruby
undefined
将同步方法与带
_later
后缀的异步任务入队方法配对使用。
ruby
undefined

app/models/card/readable.rb

app/models/card/readable.rb

def remove_inaccessible_notifications

Sync implementation

end
private def remove_inaccessible_notifications_later Card::RemoveInaccessibleNotificationsJob.perform_later(self) end
def remove_inaccessible_notifications

同步实现

end
private def remove_inaccessible_notifications_later Card::RemoveInaccessibleNotificationsJob.perform_later(self) end

app/jobs/card/remove_inaccessible_notifications_job.rb

app/jobs/card/remove_inaccessible_notifications_job.rb

class Card::RemoveInaccessibleNotificationsJob < ApplicationJob def perform(card) card.remove_inaccessible_notifications end end
undefined
class Card::RemoveInaccessibleNotificationsJob < ApplicationJob def perform(card) card.remove_inaccessible_notifications end end
undefined

thin-jobs - @rules/thin-jobs.md

thin-jobs - @rules/thin-jobs.md

Jobs call model methods. All logic lives in models.
ruby
undefined
任务仅调用模型方法,所有业务逻辑都存放在模型中。
ruby
undefined

Bad: Logic in job

不良示例:逻辑写在任务中

class ProcessOrderJob < ApplicationJob def perform(order) order.items.each { |i| i.product.decrement!(:stock) } order.update!(status: :processing) end end
class ProcessOrderJob < ApplicationJob def perform(order) order.items.each { |i| i.product.decrement!(:stock) } order.update!(status: :processing) end end

Good: Job delegates to model

良好示例:任务委托给模型

class ProcessOrderJob < ApplicationJob def perform(order) order.process # Single method call end end
undefined
class ProcessOrderJob < ApplicationJob def perform(order) order.process # 单一方法调用 end end
undefined

Controllers (HIGH)

控制器(高优先级)

resource-controllers - @rules/resource-controllers.md

resource-controllers - @rules/resource-controllers.md

Create resource controllers for state changes, not custom actions.
ruby
undefined
为状态变更创建资源控制器,而非自定义动作。
ruby
undefined

Bad: Custom actions

不良示例:自定义动作

resources :cards do post :close post :reopen end
resources :cards do post :close post :reopen end

Good: Resource controllers

良好示例:资源控制器

resources :cards do resource :closure, only: [:create, :destroy] end
resources :cards do resource :closure, only: [:create, :destroy] end

app/controllers/cards/closures_controller.rb

app/controllers/cards/closures_controller.rb

class Cards::ClosuresController < ApplicationController def create @card.close end
def destroy @card.reopen end end
undefined
class Cards::ClosuresController < ApplicationController def create @card.close end
def destroy @card.reopen end end
undefined

scoping-concerns - @rules/scoping-concerns.md

scoping-concerns - @rules/scoping-concerns.md

Use concerns like
CardScoped
for nested resource setup.
ruby
undefined
使用
CardScoped
这类关注点来处理嵌套资源的初始化。
ruby
undefined

app/controllers/concerns/card_scoped.rb

app/controllers/concerns/card_scoped.rb

module CardScoped extend ActiveSupport::Concern
included do before_action :set_card end
private def set_card @card = Current.user.accessible_cards.find_by!(number: params[:card_id]) end end
module CardScoped extend ActiveSupport::Concern
included do before_action :set_card end
private def set_card @card = Current.user.accessible_cards.find_by!(number: params[:card_id]) end end

Usage

使用示例

class Cards::CommentsController < ApplicationController include CardScoped end
undefined
class Cards::CommentsController < ApplicationController include CardScoped end
undefined

thin-controllers - @rules/thin-controllers.md

thin-controllers - @rules/thin-controllers.md

Controllers call rich model APIs directly. No service objects.
ruby
undefined
控制器直接调用功能丰富的模型API,不使用服务对象。
ruby
undefined

Good: Thin controller, rich model

良好示例:轻量控制器,功能丰富的模型

class Cards::ClosuresController < ApplicationController include CardScoped
def create @card.close # All logic in model end end
undefined
class Cards::ClosuresController < ApplicationController include CardScoped
def create @card.close # 所有逻辑在模型中 end end
undefined

Request Context (MEDIUM)

请求上下文(中优先级)

current-attributes - @rules/current-attributes.md

current-attributes - @rules/current-attributes.md

Use
Current
for request-scoped data with cascading setters.
ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :session, :user, :account

  def session=(value)
    super(value)
    self.user = session&.user
  end
end
使用
Current
来处理请求作用域的数据,并使用级联设置器。
ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :session, :user, :account

  def session=(value)
    super(value)
    self.user = session&.user
  end
end

current-in-other-contexts - @rules/current-in-other-contexts.md

current-in-other-contexts - @rules/current-in-other-contexts.md

Current
is only auto-populated in web requests. Jobs, mailers, and channels need explicit setup.
ruby
undefined
Current
仅在Web请求中自动填充。任务、邮件器和通道需要显式设置。
ruby
undefined

Jobs: extend ActiveJob to serialize/restore Current.account

任务:扩展ActiveJob以序列化/恢复Current.account

Mailers from jobs: wrap in Current.with_account { mailer.deliver }

来自任务的邮件器:用Current.with_account { mailer.deliver }包裹

Channels: set Current in Connection#connect

通道:在Connection#connect中设置Current

undefined
undefined

Associations & Callbacks (MEDIUM)

关联与回调(中优先级)

association-extensions - @rules/association-extensions.md

association-extensions - @rules/association-extensions.md

Choose between association extensions and model class methods based on context needs.
ruby
undefined
根据上下文需求选择关联扩展或模型类方法。
ruby
undefined

Use extension when you need parent context (proxy_association.owner)

当需要父上下文时使用扩展(proxy_association.owner)

has_many :accesses do def grant_to(users) board = proxy_association.owner Access.insert_all(users.map { |u| { user_id: u.id, board_id: board.id, account_id: board.account_id } }) end end
has_many :accesses do def grant_to(users) board = proxy_association.owner Access.insert_all(users.map { |u| { user_id: u.id, board_id: board.id, account_id: board.account_id } }) end end

Use class method when operation is independent

当操作独立时使用类方法

class Access def self.grant(board:, users:) insert_all(users.map { |u| { user_id: u.id, board_id: board.id } }) end end
undefined
class Access def self.grant(board:, users:) insert_all(users.map { |u| { user_id: u.id, board_id: board.id } }) end end
undefined

callbacks-patterns - @rules/callbacks-patterns.md

callbacks-patterns - @rules/callbacks-patterns.md

Use
after_commit
for jobs, inline lambdas for simple ops.
ruby
undefined
使用
after_commit
触发任务,使用内联lambda处理简单操作。
ruby
undefined

Jobs: after_commit

任务:使用after_commit

after_create_commit :notify_recipients_later
after_create_commit :notify_recipients_later

Simple ops: inline lambda

简单操作:内联lambda

after_save -> { board.touch }, if: :published?
after_save -> { board.touch }, if: :published?

Conditional: remember and check pattern

条件判断:记录变更并检查的模式

before_update :remember_changes after_update_commit :process_changes, if: :should_process?
undefined
before_update :remember_changes after_update_commit :process_changes, if: :should_process?
undefined

Turbo & Real-time (MEDIUM)

Turbo & 实时功能(中优先级)

turbo-broadcasts - @rules/turbo-broadcasts.md

turbo-broadcasts - @rules/turbo-broadcasts.md

Explicit broadcasts from controllers, not callbacks.
ruby
undefined
从控制器显式触发广播,而非通过回调。
ruby
undefined

app/models/message/broadcasts.rb

app/models/message/broadcasts.rb

module Message::Broadcasts def broadcast_create broadcast_append_to room, :messages, target: [room, :messages] end end
module Message::Broadcasts def broadcast_create broadcast_append_to room, :messages, target: [room, :messages] end end

Controller calls explicitly

控制器显式调用

def create @message = @room.messages.create!(message_params) @message.broadcast_create end
undefined
def create @message = @room.messages.create!(message_params) @message.broadcast_create end
undefined

Testing (MEDIUM)

测试(中优先级)

fixtures-testing - @rules/fixtures-testing.md

fixtures-testing - @rules/fixtures-testing.md

Use fixtures, not factories. Mirror concern structure in tests.
ruby
undefined
使用fixture而非工厂。在测试中镜像关注点的结构。
ruby
undefined

test/fixtures/cards.yml

test/fixtures/cards.yml

logo: title: The logo isn't big enough board: writebook creator: david
logo: title: The logo isn't big enough board: writebook creator: david

test/models/card/closeable_test.rb

test/models/card/closeable_test.rb

class Card::CloseableTest < ActiveSupport::TestCase test "close creates closure" do card = cards(:logo) assert_difference -> { Closure.count } do card.close end end end
undefined
class Card::CloseableTest < ActiveSupport::TestCase test "close creates closure" do card = cards(:logo) assert_difference -> { Closure.count } do card.close end end end
undefined

Code Organization (LOW-MEDIUM)

代码组织(低-中优先级)

nested-service-objects - @rules/nested-service-objects.md

nested-service-objects - @rules/nested-service-objects.md

Place service objects under model namespace, not
app/services
.
ruby
undefined
将服务对象放在模型命名空间下,而非
app/services
目录。
ruby
undefined

Good: app/models/card/activity_spike/detector.rb

良好示例:app/models/card/activity_spike/detector.rb

class Card::ActivitySpike::Detector def initialize(card) @card = card end
def detect # ... end end
undefined
class Card::ActivitySpike::Detector def initialize(card) @card = card end
def detect # ... end end
undefined

code-style - @rules/code-style.md

code-style - @rules/code-style.md

Prefer expanded conditionals, order methods by invocation.
ruby
undefined
偏好展开式条件判断,按调用顺序排列方法。
ruby
undefined

Expanded conditionals

展开式条件判断

def find_record if record = find_by_id(id) record else NullRecord.new end end
def find_record if record = find_by_id(id) record else NullRecord.new end end

Method ordering: caller before callees

方法排序:调用方在被调用方之前

def process step_one step_two end
private def step_one; end def step_two; end
undefined
def process step_one step_two end
private def step_one; end def step_two; end
undefined

Philosophy

设计理念

These patterns embody "Vanilla Rails" - using Rails conventions with minimal additions:
  1. Rich models, thin controllers - Domain logic in models and concerns
  2. No service object layer - Controllers talk to models directly
  3. Co-located code - Concerns, jobs, and services near the models they serve
  4. Explicit over implicit - Call broadcasts explicitly, not via callbacks
  5. Convention over configuration - Follow naming patterns for predictability
这些模式体现了「原生Rails」的思想——在Rails约定的基础上仅做最小化扩展:
  1. 功能丰富的模型,轻量控制器 - 领域逻辑存放在模型和关注点中
  2. 无服务对象层 - 控制器直接与模型交互
  3. 代码就近存放 - 关注点、任务和服务对象紧邻其服务的模型
  4. 显式优于隐式 - 显式调用广播,而非通过回调
  5. 约定优于配置 - 遵循命名模式以提升可预测性