dhh-ruby-style

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

DHH Ruby/Rails Style Guide

DHH Ruby/Rails 风格指南

Write Ruby and Rails code following DHH's philosophy: clarity over cleverness, convention over configuration, developer happiness above all.
遵循DHH的理念编写Ruby和Rails代码:清晰胜于精巧约定优于配置开发者幸福感至上

Quick Reference

快速参考

Controller Actions

控制器动作

  • Only 7 REST actions:
    index
    ,
    show
    ,
    new
    ,
    create
    ,
    edit
    ,
    update
    ,
    destroy
  • New behavior? Create a new controller, not a custom action
  • Action length: 1-5 lines maximum
  • Empty actions are fine: Let Rails convention handle rendering
ruby
class MessagesController < ApplicationController
  before_action :set_message, only: %i[ show edit update destroy ]

  def index
    @messages = @room.messages.with_creator.last_page
    fresh_when @messages
  end

  def show
  end

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

  private
    def set_message
      @message = @room.messages.find(params[:id])
    end

    def message_params
      params.require(:message).permit(:body, :attachment)
    end
end
  • 仅使用7个REST动作
    index
    ,
    show
    ,
    new
    ,
    create
    ,
    edit
    ,
    update
    ,
    destroy
  • 需要新行为? 创建新控制器,而非自定义动作
  • 动作长度:最多1-5行
  • 空动作是允许的:让Rails的约定来处理渲染
ruby
class MessagesController < ApplicationController
  before_action :set_message, only: %i[ show edit update destroy ]

  def index
    @messages = @room.messages.with_creator.last_page
    fresh_when @messages
  end

  def show
  end

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

  private
    def set_message
      @message = @room.messages.find(params[:id])
    end

    def message_params
      params.require(:message).permit(:body, :attachment)
    end
end

Private Method Indentation

私有方法缩进

Indent private methods one level under
private
keyword:
ruby
  private
    def set_message
      @message = Message.find(params[:id])
    end

    def message_params
      params.require(:message).permit(:body)
    end
私有方法在
private
关键字下缩进一级:
ruby
  private
    def set_message
      @message = Message.find(params[:id])
    end

    def message_params
      params.require(:message).permit(:body)
    end

Model Design (Fat Models)

模型设计(胖模型)

Models own business logic, authorization, and broadcasting:
ruby
class Message < ApplicationRecord
  belongs_to :room
  belongs_to :creator, class_name: "User"
  has_many :mentions

  scope :with_creator, -> { includes(:creator) }
  scope :page_before, ->(cursor) { where("id < ?", cursor.id).order(id: :desc).limit(50) }

  def broadcast_create
    broadcast_append_to room, :messages, target: "messages"
  end

  def mentionees
    mentions.includes(:user).map(&:user)
  end
end

class User < ApplicationRecord
  def can_administer?(message)
    message.creator == self || admin?
  end
end
模型负责业务逻辑、授权和广播:
ruby
class Message < ApplicationRecord
  belongs_to :room
  belongs_to :creator, class_name: "User"
  has_many :mentions

  scope :with_creator, -> { includes(:creator) }
  scope :page_before, ->(cursor) { where("id < ?", cursor.id).order(id: :desc).limit(50) }

  def broadcast_create
    broadcast_append_to room, :messages, target: "messages"
  end

  def mentionees
    mentions.includes(:user).map(&:user)
  end
end

class User < ApplicationRecord
  def can_administer?(message)
    message.creator == self || admin?
  end
end

Current Attributes

Current 属性

Use
Current
for request context, never pass
current_user
everywhere:
ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :user, :session
end
使用
Current
存储请求上下文,不要到处传递
current_user
ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :user, :session
end

Usage anywhere in app

在应用的任何地方使用

Current.user.can_administer?(@message)
undefined
Current.user.can_administer?(@message)
undefined

Ruby Syntax Preferences

Ruby 语法偏好

ruby
undefined
ruby
undefined

Symbol arrays with spaces inside brackets

符号数组在括号内添加空格

before_action :set_message, only: %i[ show edit update destroy ]
before_action :set_message, only: %i[ show edit update destroy ]

Modern hash syntax exclusively

仅使用现代哈希语法

params.require(:message).permit(:body, :attachment)
params.require(:message).permit(:body, :attachment)

Single-line blocks with braces

单行块使用大括号

users.each { |user| user.notify }
users.each { |user| user.notify }

Ternaries for simple conditionals

简单条件使用三元运算符

@room.direct? ? @room.users : @message.mentionees
@room.direct? ? @room.users : @message.mentionees

Bang methods for fail-fast

使用 bang 方法快速失败

@message = Message.create!(params) @message.update!(message_params)
@message = Message.create!(params) @message.update!(message_params)

Predicate methods with question marks

谓词方法以问号结尾

@room.direct? user.can_administer?(@message) @messages.any?
@room.direct? user.can_administer?(@message) @messages.any?

Expression-less case for cleaner conditionals

无表达式的case语句让条件更简洁

case when params[:before].present? @room.messages.page_before(params[:before]) when params[:after].present? @room.messages.page_after(params[:after]) else @room.messages.last_page end
undefined
case when params[:before].present? @room.messages.page_before(params[:before]) when params[:after].present? @room.messages.page_after(params[:after]) else @room.messages.last_page end
undefined

Naming Conventions

命名约定

ElementConventionExample
Setter methods
set_
prefix
set_message
,
set_room
Parameter methods
{model}_params
message_params
Association namesSemantic, not generic
creator
not
user
ScopesChainable, descriptive
with_creator
,
page_before
PredicatesEnd with
?
direct?
,
can_administer?
元素约定示例
设置方法
set_
为前缀
set_message
,
set_room
参数方法
{model}_params
格式
message_params
关联名称语义化而非通用
creator
而非
user
作用域可链式调用、描述性强
with_creator
,
page_before
谓词方法
?
结尾
direct?
,
can_administer?

Hotwire/Turbo Patterns

Hotwire/Turbo 模式

Broadcasting is model responsibility:
ruby
undefined
广播是模型的职责:
ruby
undefined

In model

在模型中

def broadcast_create broadcast_append_to room, :messages, target: "messages" end
def broadcast_create broadcast_append_to room, :messages, target: "messages" end

In controller

在控制器中

@message.broadcast_replace_to @room, :messages, target: [ @message, :presentation ], partial: "messages/presentation", attributes: { maintain_scroll: true }
undefined
@message.broadcast_replace_to @room, :messages, target: [ @message, :presentation ], partial: "messages/presentation", attributes: { maintain_scroll: true }
undefined

Error Handling

错误处理

Rescue specific exceptions, fail fast with bang methods:
ruby
def create
  @message = @room.messages.create_with_attachment!(message_params)
  @message.broadcast_create
rescue ActiveRecord::RecordNotFound
  render action: :room_not_found
end
捕获特定异常,使用bang方法快速失败:
ruby
def create
  @message = @room.messages.create_with_attachment!(message_params)
  @message.broadcast_create
rescue ActiveRecord::RecordNotFound
  render action: :room_not_found
end

Architecture Preferences

架构偏好

TraditionalDHH Way
PostgreSQLSQLite (for single-tenant)
Redis + SidekiqSolid Queue
Redis cacheSolid Cache
KubernetesSingle Docker container
Service objectsFat models
Policy objects (Pundit)Authorization on User model
FactoryBotFixtures
传统方案DHH 方案
PostgreSQLSQLite(单租户场景)
Redis + SidekiqSolid Queue
Redis 缓存Solid Cache
Kubernetes单个Docker容器
服务对象胖模型
策略对象(Pundit)在User模型中处理授权
FactoryBot测试夹具(Fixtures)

Detailed References

详细参考

For comprehensive patterns and examples, see:
  • references/patterns.md
    - Complete code patterns with explanations
  • references/resources.md
    - Links to source material and further reading
如需完整的模式和示例,请查看:
  • references/patterns.md
    - 带解释的完整代码模式
  • references/resources.md
    - 源材料链接和扩展阅读

Philosophy Summary

理念总结

  1. REST purity: 7 actions only; new controllers for variations
  2. Fat models: Authorization, broadcasting, business logic in models
  3. Thin controllers: 1-5 line actions; extract complexity
  4. Convention over configuration: Empty methods, implicit rendering
  5. Minimal abstractions: No service objects for simple cases
  6. Current attributes: Thread-local request context everywhere
  7. Hotwire-first: Model-level broadcasting, Turbo Streams, Stimulus
  8. Readable code: Semantic naming, small methods, no comments needed
  9. Pragmatic testing: System tests over unit tests, real integrations
  1. 纯粹REST:仅使用7个动作;为变体创建新控制器
  2. 胖模型:授权、广播、业务逻辑都放在模型中
  3. 瘦控制器:动作1-5行;提取复杂逻辑
  4. 约定优于配置:空方法、隐式渲染
  5. 最小化抽象:简单场景不使用服务对象
  6. Current 属性:线程本地的请求上下文可全局使用
  7. 优先使用Hotwire:模型级广播、Turbo Streams、Stimulus
  8. 可读代码:语义化命名、小方法、无需注释
  9. 务实测试:优先系统测试而非单元测试,真实集成测试