ruby-on-rails-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRuby 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 not .
app/models/model_name/app/models/concerns/ruby
undefined将模型专属的关注点放置在目录下,而非。
app/models/model_name/app/models/concerns/ruby
undefinedDirectory 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
undefinedclass Card < ApplicationRecord
include Closeable, Searchable, Assignable
Ruby会优先从Card::命名空间解析
end
undefinedconcern-naming - @rules/concern-naming.md
concern-naming - @rules/concern-naming.md
Use suffix for behavior concerns, nouns for feature concerns.
-ableruby
undefined行为类关注点使用后缀,功能类关注点使用名词。
-ableruby
undefinedBehaviors: -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
undefinedmodule User::Avatar # 包含头像功能
module User::Role # 包含角色功能
module Card::Mentions # 包含@提及功能
undefinedtemplate-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
undefinedapp/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
undefinedmodule Card::Searchable
include ::Searchable
def search_title
title # 实现钩子方法
end
end
undefinedBackground Jobs (HIGH)
后台任务(高优先级)
paired-async-methods - @rules/paired-async-methods.md
paired-async-methods - @rules/paired-async-methods.md
Pair sync methods with variants that enqueue jobs.
_laterruby
undefined将同步方法与带后缀的异步任务入队方法配对使用。
_laterruby
undefinedapp/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
undefinedclass Card::RemoveInaccessibleNotificationsJob < ApplicationJob
def perform(card)
card.remove_inaccessible_notifications
end
end
undefinedthin-jobs - @rules/thin-jobs.md
thin-jobs - @rules/thin-jobs.md
Jobs call model methods. All logic lives in models.
ruby
undefined任务仅调用模型方法,所有业务逻辑都存放在模型中。
ruby
undefinedBad: 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
undefinedclass ProcessOrderJob < ApplicationJob
def perform(order)
order.process # 单一方法调用
end
end
undefinedControllers (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
undefinedBad: 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
undefinedclass Cards::ClosuresController < ApplicationController
def create
@card.close
end
def destroy
@card.reopen
end
end
undefinedscoping-concerns - @rules/scoping-concerns.md
scoping-concerns - @rules/scoping-concerns.md
Use concerns like for nested resource setup.
CardScopedruby
undefined使用这类关注点来处理嵌套资源的初始化。
CardScopedruby
undefinedapp/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
undefinedclass Cards::CommentsController < ApplicationController
include CardScoped
end
undefinedthin-controllers - @rules/thin-controllers.md
thin-controllers - @rules/thin-controllers.md
Controllers call rich model APIs directly. No service objects.
ruby
undefined控制器直接调用功能丰富的模型API,不使用服务对象。
ruby
undefinedGood: Thin controller, rich model
良好示例:轻量控制器,功能丰富的模型
class Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close # All logic in model
end
end
undefinedclass Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close # 所有逻辑在模型中
end
end
undefinedRequest Context (MEDIUM)
请求上下文(中优先级)
current-attributes - @rules/current-attributes.md
current-attributes - @rules/current-attributes.md
Use for request-scoped data with cascading setters.
Currentruby
class Current < ActiveSupport::CurrentAttributes
attribute :session, :user, :account
def session=(value)
super(value)
self.user = session&.user
end
end使用来处理请求作用域的数据,并使用级联设置器。
Currentruby
class Current < ActiveSupport::CurrentAttributes
attribute :session, :user, :account
def session=(value)
super(value)
self.user = session&.user
end
endcurrent-in-other-contexts - @rules/current-in-other-contexts.md
current-in-other-contexts - @rules/current-in-other-contexts.md
Currentruby
undefinedCurrentruby
undefinedJobs: 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
undefinedundefinedAssociations & 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
undefinedUse 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
undefinedclass Access
def self.grant(board:, users:)
insert_all(users.map { |u| { user_id: u.id, board_id: board.id } })
end
end
undefinedcallbacks-patterns - @rules/callbacks-patterns.md
callbacks-patterns - @rules/callbacks-patterns.md
Use for jobs, inline lambdas for simple ops.
after_commitruby
undefined使用触发任务,使用内联lambda处理简单操作。
after_commitruby
undefinedJobs: 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?
undefinedbefore_update :remember_changes
after_update_commit :process_changes, if: :should_process?
undefinedTurbo & 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
undefinedapp/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
undefineddef create
@message = @room.messages.create!(message_params)
@message.broadcast_create
end
undefinedTesting (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
undefinedtest/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
undefinedclass Card::CloseableTest < ActiveSupport::TestCase
test "close creates closure" do
card = cards(:logo)
assert_difference -> { Closure.count } do
card.close
end
end
end
undefinedCode 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/servicesruby
undefined将服务对象放在模型命名空间下,而非目录。
app/servicesruby
undefinedGood: 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
undefinedclass Card::ActivitySpike::Detector
def initialize(card)
@card = card
end
def detect
# ...
end
end
undefinedcode-style - @rules/code-style.md
code-style - @rules/code-style.md
Prefer expanded conditionals, order methods by invocation.
ruby
undefined偏好展开式条件判断,按调用顺序排列方法。
ruby
undefinedExpanded 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
undefineddef process
step_one
step_two
end
private
def step_one; end
def step_two; end
undefinedPhilosophy
设计理念
These patterns embody "Vanilla Rails" - using Rails conventions with minimal additions:
- Rich models, thin controllers - Domain logic in models and concerns
- No service object layer - Controllers talk to models directly
- Co-located code - Concerns, jobs, and services near the models they serve
- Explicit over implicit - Call broadcasts explicitly, not via callbacks
- Convention over configuration - Follow naming patterns for predictability
这些模式体现了「原生Rails」的思想——在Rails约定的基础上仅做最小化扩展:
- 功能丰富的模型,轻量控制器 - 领域逻辑存放在模型和关注点中
- 无服务对象层 - 控制器直接与模型交互
- 代码就近存放 - 关注点、任务和服务对象紧邻其服务的模型
- 显式优于隐式 - 显式调用广播,而非通过回调
- 约定优于配置 - 遵循命名模式以提升可预测性