jr-rails-classic

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Rails 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

核心工作流

  1. Use generators first
    rails g model
    ,
    rails g controller
    ,
    rails g migration
    . Generators produce correct file structure, test stubs, and route entries in one shot.
  2. Implementation order — models → controllers → views → tests.
  3. Ship, Validate, Refine — prototype to production, learn from real usage.
  4. Let it crash — use bang methods, let Rails handle RecordInvalid with 422s.
  1. 优先使用生成器
    rails g model
    rails g controller
    rails g migration
    。生成器可以一次性创建正确的文件结构、测试桩和路由条目。
  2. 实现顺序 — 模型 → 控制器 → 视图 → 测试。
  3. 发布、验证、优化 — 从原型到生产环境,从实际使用中迭代优化。
  4. 允许崩溃 — 使用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
    strftime
    in views — custom DATE_FORMATS in initializers
  • 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替代
  • 视图中不使用
    strftime
    — 在initializers中自定义DATE_FORMATS统一处理
  • 回调仅用于派生数据和异步调度 — 永远不要编写业务逻辑
  • 硬约束优先使用数据库约束而非模型校验
  • 给任务传递ID,而非对象实例

Conventions Quick Reference

约定快速参考

Naming

命名规则

  • Verbs for state changes:
    card.close
    ,
    board.publish
  • Predicates from record presence:
    card.closed?
    ,
    card.golden?
  • Concerns as adjectives:
    Closeable
    ,
    Publishable
    ,
    Watchable
  • Controllers as nouns:
    Cards::ClosuresController
  • Scopes as business terms:
    scope :active
    ,
    scope :chronologically
  • 状态变更方法用动词
    card.close
    board.publish
  • 记录存在性判断用谓词方法
    card.closed?
    card.golden?
  • Concern用形容词命名:
    Closeable
    Publishable
    Watchable
  • 控制器用名词命名:
    Cards::ClosuresController
  • 作用域用业务术语命名:
    scope :active
    scope :chronologically

REST Mapping

REST映射

No custom actions. Create sub-resources:
ActionRoute
close a card
POST /cards/:id/closure
archive a card
POST /cards/:id/archival
watch a board
POST /boards/:id/watching
不使用自定义动作,创建子资源替代:
操作路由
关闭卡片
POST /cards/:id/closure
归档卡片
POST /cards/:id/archival
关注看板
POST /boards/:id/watching

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
end

Querying

查询示例

Card.joins(:closure) # closed Card.where.missing(:closure) # open
undefined
Card.joins(:closure) # 已关闭的卡片 Card.where.missing(:closure) # 未关闭的卡片
undefined

Concerns — 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
end

POROs Under Model Namespace

模型命名空间下的PORO

Business logic that doesn't fit a concern:
ruby
undefined
不适合放在concern中的业务逻辑:
ruby
undefined

app/models/event/description.rb

app/models/event/description.rb

class Event::Description def initialize(event) = @event = event def to_s = # ... end
undefined
class Event::Description def initialize(event) = @event = event def to_s = # ... end
undefined

Validations

校验规则

Minimal on model (data integrity), contextual on form objects (UI flows):
ruby
undefined
模型层仅做最少的校验(保证数据完整性),表单对象层做上下文校验(适配UI流程):
ruby
undefined

Model — 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
undefined
add_index :users, :email, unique: true add_foreign_key :cards, :boards
undefined

Association Design

关联设计

Instead ofPreferWhen
has_and_belongs_to_many
has_many :through
Join model needs attributes or callbacks
1:NM:N (
has_many :through
)
Relationship may grow
polymorphic: true
delegated_type
Type-safe variants with different schemas
Always set
:dependent
on
has_many
/
has_one
. Use
:inverse_of
when Rails can't infer it.
不推荐推荐适用场景
has_and_belongs_to_many
has_many :through
关联模型需要属性或回调时
1:N关系M:N关系(
has_many :through
关系未来可能扩展时
polymorphic: true
delegated_type
不同 schema 的类型安全变体
has_many
/
has_one
关联必须设置
:dependent
属性,当Rails无法自动推断时使用
:inverse_of

Rails 7.1+ Patterns

Rails 7.1+ 适配模式

ruby
undefined
ruby
undefined

Delegated 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
undefined
class User < ApplicationRecord store_accessor :settings, :theme, :notifications_enabled end
undefined

Database

数据库相关

ruby
undefined
ruby
undefined

UUIDs (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
undefined
class Card < ApplicationRecord belongs_to :creator, class_name: "User", default: -> { Current.user } end
undefined

Controllers

控制器

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
end

Current Attributes

Current 属性

ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :session, :user, :account, :request_id
  delegate :user, to: :session, allow_nil: true
end
ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :session, :user, :account, :request_id
  delegate :user, to: :session, allow_nil: true
end

Background 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
end

View Helpers

视图助手

Date formatting via initializer (never
strftime
in views):
ruby
undefined
通过initializer统一处理日期格式化(视图中永远不要使用
strftime
):
ruby
undefined

config/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

推荐技术栈

ConcernGem
Frontendturbo-rails, stimulus-rails, importmap-rails
Assetspropshaft
JobsSolid Queue
Cache/CableSolid Cache, Solid Cable
AuthorizationPundit
DeploymentKamal + Thruster
功能模块Gem
前端turbo-rails, stimulus-rails, importmap-rails
资产处理propshaft
任务调度Solid Queue
缓存/长连接Solid Cache, Solid Cable
权限控制Pundit
部署Kamal + Thruster

Gem Selection

Gem 选择原则

  1. Can vanilla Rails do this?
  2. Is it the app's core concern? If yes, own the code
  3. Does it add infrastructure? Database-backed alternatives exist
  4. Is the complexity worth it?
  5. Is it from someone you trust?
  1. 原生Rails是否能实现该功能?
  2. 是否属于应用核心功能?如果是,自行实现代码
  3. 是否需要额外基础设施?优先选择基于数据库的替代方案
  4. 复杂度是否匹配收益?
  5. 作者是否值得信赖?

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
hwc-*
skill alongside this one.
当任务需要更深入的指引时按需阅读:
  • 设计模式(表单对象、查询对象、策略模式等):reference/patterns.md
  • 反模式与代码异味reference/anti-patterns.mdreference/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)相关指引,可搭配
hwc-*
相关skill使用。