jr-rails-classic
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRails Classic Coding Style
Rails 经典编码风格
Write Rails application code following 37signals conventions. Rich domain
models, CRUD controllers, database-backed everything, Minitest with fixtures.
遵循37signals约定编写Rails应用代码,包含富领域模型、CRUD控制器、所有逻辑基于数据库实现、搭配fixtures的Minitest测试。
Core Workflow
核心工作流
- Use generators first — ,
rails g model,rails g controller. Generators produce correct file structure, test stubs, and route entries in one shot.rails g migration - Implementation order — models → controllers → views → tests.
- Ship, Validate, Refine — prototype to production, learn from real usage.
- Let it crash — use bang methods, let Rails handle RecordInvalid with 422s.
- 优先使用生成器 — 、
rails g model、rails g controller。生成器可以一次性创建正确的文件结构、测试桩和路由条目。rails g migration - 实现顺序 — 模型 → 控制器 → 视图 → 测试。
- 发布、验证、优化 — 从原型到生产环境,从实际使用中迭代优化。
- 允许崩溃 — 使用bang方法,让Rails自行处理RecordInvalid异常返回422状态码。
Guardrails
规范约束
- No service objects — use domain models namespaced under
app/models/ - No custom controller actions — create new resources instead (REST mapping)
- No RSpec, no factory_bot — Minitest with fixtures only
- No Redis — Solid Queue, Solid Cache, Solid Cable
- No in views — custom DATE_FORMATS in initializers
strftime - Callbacks only for derived data and async dispatch — never business logic
- Database constraints over model validations for hard guarantees
- Pass IDs to jobs, not objects
- 不使用service对象 — 业务逻辑放在命名空间下的领域模型中
app/models/ - 不使用自定义控制器动作 — 改为创建新的资源(遵循REST映射)
- 不使用RSpec、factory_bot — 仅使用搭配fixtures的Minitest
- 不使用Redis — 采用Solid Queue、Solid Cache、Solid Cable替代
- 视图中不使用— 在initializers中自定义DATE_FORMATS统一处理
strftime - 回调仅用于派生数据和异步调度 — 永远不要编写业务逻辑
- 硬约束优先使用数据库约束而非模型校验
- 给任务传递ID,而非对象实例
Conventions Quick Reference
约定快速参考
Naming
命名规则
- Verbs for state changes: ,
card.closeboard.publish - Predicates from record presence: ,
card.closed?card.golden? - Concerns as adjectives: ,
Closeable,PublishableWatchable - Controllers as nouns:
Cards::ClosuresController - Scopes as business terms: ,
scope :activescope :chronologically
- 状态变更方法用动词:、
card.closeboard.publish - 记录存在性判断用谓词方法:、
card.closed?card.golden? - Concern用形容词命名:、
Closeable、PublishableWatchable - 控制器用名词命名:
Cards::ClosuresController - 作用域用业务术语命名:、
scope :activescope :chronologically
REST Mapping
REST映射
No custom actions. Create sub-resources:
| Action | Route |
|---|---|
| close a card | |
| archive a card | |
| watch a board | |
不使用自定义动作,创建子资源替代:
| 操作 | 路由 |
|---|---|
| 关闭卡片 | |
| 归档卡片 | |
| 关注看板 | |
Models — State as Records
模型 — 状态即记录
Instead of booleans, create state records (timestamps + authorship for free):
ruby
class Card::Closure < ApplicationRecord
belongs_to :card
belongs_to :creator, class_name: "User"
end
module Closeable
extend ActiveSupport::Concern
included do
has_one :closure, dependent: :destroy
end
def closed? = closure.present?
def close(creator: Current.user) = create_closure!(creator: creator)
def reopen = closure&.destroy
end不要用布尔字段,改为创建状态记录(自动附带时间戳和作者信息):
ruby
class Card::Closure < ApplicationRecord
belongs_to :card
belongs_to :creator, class_name: "User"
end
module Closeable
extend ActiveSupport::Concern
included do
has_one :closure, dependent: :destroy
end
def closed? = closure.present?
def close(creator: Current.user) = create_closure!(creator: creator)
def reopen = closure&.destroy
endQuerying
查询示例
Card.joins(:closure) # closed
Card.where.missing(:closure) # open
undefinedCard.joins(:closure) # 已关闭的卡片
Card.where.missing(:closure) # 未关闭的卡片
undefinedConcerns — Horizontal Behavior
Concerns — 横向行为
Self-contained (associations + scopes + methods). 50–150 lines. Named for
capabilities, not organization.
ruby
class Card < ApplicationRecord
include Assignable, Closeable, Golden, Watchable, Searchable
end功能自包含(包含关联、作用域、方法),代码量控制在50-150行,按功能命名而非组织结构命名。
ruby
class Card < ApplicationRecord
include Assignable, Closeable, Golden, Watchable, Searchable
endPOROs Under Model Namespace
模型命名空间下的PORO
Business logic that doesn't fit a concern:
ruby
undefined不适合放在concern中的业务逻辑:
ruby
undefinedapp/models/event/description.rb
app/models/event/description.rb
class Event::Description
def initialize(event) = @event = event
def to_s = # ...
end
undefinedclass Event::Description
def initialize(event) = @event = event
def to_s = # ...
end
undefinedValidations
校验规则
Minimal on model (data integrity), contextual on form objects (UI flows):
ruby
undefined模型层仅做最少的校验(保证数据完整性),表单对象层做上下文校验(适配UI流程):
ruby
undefinedModel — minimal
模型层 — 最小校验
class User < ApplicationRecord
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
normalizes :email, with: ->(e) { e.strip.downcase }
end
class User < ApplicationRecord
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
normalizes :email, with: ->(e) { e.strip.downcase }
end
Migration — hard constraints
迁移文件 — 硬约束
add_index :users, :email, unique: true
add_foreign_key :cards, :boards
undefinedadd_index :users, :email, unique: true
add_foreign_key :cards, :boards
undefinedAssociation Design
关联设计
| Instead of | Prefer | When |
|---|---|---|
| | Join model needs attributes or callbacks |
| 1:N | M:N ( | Relationship may grow |
| | Type-safe variants with different schemas |
Always set on /. Use when
Rails can't infer it.
:dependenthas_manyhas_one:inverse_of| 不推荐 | 推荐 | 适用场景 |
|---|---|---|
| | 关联模型需要属性或回调时 |
| 1:N关系 | M:N关系( | 关系未来可能扩展时 |
| | 不同 schema 的类型安全变体 |
has_manyhas_one:dependent:inverse_ofRails 7.1+ Patterns
Rails 7.1+ 适配模式
ruby
undefinedruby
undefinedDelegated types
委托类型
class Message < ApplicationRecord
delegated_type :messageable, types: %w[Comment Reply Announcement]
end
class Message < ApplicationRecord
delegated_type :messageable, types: %w[Comment Reply Announcement]
end
store_accessor for JSON/JSONB
JSON/JSONB字段的store_accessor
class User < ApplicationRecord
store_accessor :settings, :theme, :notifications_enabled
end
undefinedclass User < ApplicationRecord
store_accessor :settings, :theme, :notifications_enabled
end
undefinedDatabase
数据库相关
ruby
undefinedruby
undefinedUUIDs (UUIDv7 — time-sortable)
UUID(UUIDv7 — 支持按时间排序)
create_table :cards, id: :uuid do |t|
t.references :board, type: :uuid, foreign_key: true
end
create_table :cards, id: :uuid do |t|
t.references :board, type: :uuid, foreign_key: true
end
Counter caches
计数缓存
class Comment < ApplicationRecord
belongs_to :card, counter_cache: true
end
class Comment < ApplicationRecord
belongs_to :card, counter_cache: true
end
Default values with Current
配合Current设置默认值
class Card < ApplicationRecord
belongs_to :creator, class_name: "User", default: -> { Current.user }
end
undefinedclass Card < ApplicationRecord
belongs_to :creator, class_name: "User", default: -> { Current.user }
end
undefinedControllers
控制器
Thin. Use concerns for shared behavior:
ruby
module CardScoped
extend ActiveSupport::Concern
included { before_action :set_card }
private
def set_card
@card = Card.find(params[:card_id])
@board = @card.board
end
def render_card_replacement
render turbo_stream: turbo_stream.replace(@card)
end
end
class Cards::ClosuresController < ApplicationController
include CardScoped
def create = @card.close && render_card_replacement
def destroy = @card.reopen && render_card_replacement
end保持轻量化,用concern封装共享逻辑:
ruby
module CardScoped
extend ActiveSupport::Concern
included { before_action :set_card }
private
def set_card
@card = Card.find(params[:card_id])
@board = @card.board
end
def render_card_replacement
render turbo_stream: turbo_stream.replace(@card)
end
end
class Cards::ClosuresController < ApplicationController
include CardScoped
def create = @card.close && render_card_replacement
def destroy = @card.reopen && render_card_replacement
endCurrent Attributes
Current 属性
ruby
class Current < ActiveSupport::CurrentAttributes
attribute :session, :user, :account, :request_id
delegate :user, to: :session, allow_nil: true
endruby
class Current < ActiveSupport::CurrentAttributes
attribute :session, :user, :account, :request_id
delegate :user, to: :session, allow_nil: true
endBackground Jobs
后台任务
Thin wrappers — logic lives on the model:
ruby
module Watchable
def notify_watchers_later = NotifyWatchersJob.perform_later(self)
def notify_watchers
watchers.each { |w| WatcherMailer.notification(w, self).deliver_later }
end
end
class NotifyWatchersJob < ApplicationJob
def perform(card) = card.notify_watchers
end作为轻量封装存在,业务逻辑放在模型中:
ruby
module Watchable
def notify_watchers_later = NotifyWatchersJob.perform_later(self)
def notify_watchers
watchers.each { |w| WatcherMailer.notification(w, self).deliver_later }
end
end
class NotifyWatchersJob < ApplicationJob
def perform(card) = card.notify_watchers
endView Helpers
视图助手
Date formatting via initializer (never in views):
strftimeruby
undefined通过initializer统一处理日期格式化(视图中永远不要使用):
strftimeruby
undefinedconfig/initializers/date_formats.rb
config/initializers/date_formats.rb
Date::DATE_FORMATS[:default] = "%d.%m.%Y"
Time::DATE_FORMATS[:default] = "%d.%m.%Y %H:%M"
Time::DATE_FORMATS[:time_only] = "%H:%M"
Use `active_link_to` gem for navigation active states.Date::DATE_FORMATS[:default] = "%d.%m.%Y"
Time::DATE_FORMATS[:default] = "%d.%m.%Y %H:%M"
Time::DATE_FORMATS[:time_only] = "%H:%M"
使用`active_link_to` gem处理导航激活状态。Testing
测试规范
- Minitest with fixtures, integration tests for controllers
- Test observable behavior, not implementation
- Don't mock what you can test for real
- VCR for external APIs
- 搭配fixtures使用Minitest,控制器编写集成测试
- 测试可观测的行为,而非实现细节
- 能真实测试的逻辑不要mock
- 外部API调用使用VCR
Preferred Stack
推荐技术栈
| Concern | Gem |
|---|---|
| Frontend | turbo-rails, stimulus-rails, importmap-rails |
| Assets | propshaft |
| Jobs | Solid Queue |
| Cache/Cable | Solid Cache, Solid Cable |
| Authorization | Pundit |
| Deployment | Kamal + Thruster |
| 功能模块 | Gem |
|---|---|
| 前端 | turbo-rails, stimulus-rails, importmap-rails |
| 资产处理 | propshaft |
| 任务调度 | Solid Queue |
| 缓存/长连接 | Solid Cache, Solid Cable |
| 权限控制 | Pundit |
| 部署 | Kamal + Thruster |
Gem Selection
Gem 选择原则
- Can vanilla Rails do this?
- Is it the app's core concern? If yes, own the code
- Does it add infrastructure? Database-backed alternatives exist
- Is the complexity worth it?
- Is it from someone you trust?
- 原生Rails是否能实现该功能?
- 是否属于应用核心功能?如果是,自行实现代码
- 是否需要额外基础设施?优先选择基于数据库的替代方案
- 复杂度是否匹配收益?
- 作者是否值得信赖?
Deep Reference Files
深度参考文档
Read these on demand when the task requires deeper guidance:
- Design patterns (form objects, query objects, strategies, etc.): reference/patterns.md
- Anti-patterns and code smells: reference/anti-patterns.md, reference/smells.md
- Refactoring recipes: reference/refactorings/ (002–010)
- Testing guide: reference/shared/testing.md
- Hotwire (Turbo + Stimulus): reference/shared/hotwire.md
- Background jobs + Continuations: reference/shared/jobs.md
- State machines (AASM): reference/shared/state_machines.md
- Callbacks scoring + extraction: reference/shared/callbacks.md
- Authorization (Pundit): reference/shared/authorization.md
- Notifications (Noticed): reference/shared/notifications.md
- Instrumentation / EventReporter: reference/shared/instrumentation.md
- Components (Phlex): reference/shared/components.md
- Serializers (API JSON): reference/shared/serializers.md
- Architecture layers: reference/shared/architecture.md
- Gem recommendations: reference/toolbelt.md
For frontend patterns (Stimulus controllers, Turbo Frames/Streams), invoke
the relevant skill alongside this one.
hwc-*当任务需要更深入的指引时按需阅读:
- 设计模式(表单对象、查询对象、策略模式等):reference/patterns.md
- 反模式与代码异味:reference/anti-patterns.md、reference/smells.md
- 重构方案:reference/refactorings/ (002–010)
- 测试指南:reference/shared/testing.md
- Hotwire(Turbo + Stimulus):reference/shared/hotwire.md
- 后台任务 + 续跑:reference/shared/jobs.md
- 状态机(AASM):reference/shared/state_machines.md
- 回调评分与抽离:reference/shared/callbacks.md
- 权限控制(Pundit):reference/shared/authorization.md
- 通知(Noticed):reference/shared/notifications.md
- 埋点 / EventReporter:reference/shared/instrumentation.md
- 组件(Phlex):reference/shared/components.md
- 序列化器(API JSON):reference/shared/serializers.md
- 架构分层:reference/shared/architecture.md
- Gem推荐:reference/toolbelt.md
如需前端模式(Stimulus控制器、Turbo Frames/Streams)相关指引,可搭配相关skill使用。
hwc-*