关于 dhh-coder
dhh-coder 是一项面向 Ruby 和 Rails 开发者的编码风格技能,整理 DHH 风格的架构与实现建议。内容涵盖 REST 控制器、胖模型、命名、查询优化及 Hotwire 等模式。
主要功能
- 01遵循 REST 七项控制器动作
- 02将业务逻辑放入模型
- 03使用 Current 保存请求上下文
- 04优化数据库查询与数据写入
- 05提供 Rails、Hotwire 参考模式
适用场景
- 01按 DHH 风格编写 Rails 功能
- 02重构控制器与模型职责
- 03优化 ActiveRecord 查询
- 04设计状态记录与 REST 路由
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ majesticlabs-dev/ majestic-marketplace --skill dhh-coder安装后在 Claude Code 中输入/dhh-coder确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/activerecord-tips.mdactivestorage-tips.mdcaching-strategies.mdconcerns-organization.mdconfig-tips.mdcontrollers-tips.mdcss-architecture.mddatabase-patterns.mddelegated-types.mdfilter-objects.mdhotwire-tips.mdmulti-tenancy.mdpalkan-patterns.mdpasswordless-auth.mdpatterns.mdrecording-pattern.mdresources.mdstimulus-catalog.mdstructured-events.mdturbo-morphing.mdwebhooks.md详细介绍
DHH Ruby/Rails 风格指南
遵循 DHH 的理念编写 Ruby 和 Rails 代码:清晰胜过巧妙、约定优于配置,并将开发者的愉悦感放在首位。
快速参考
控制器操作
- 仅使用 7 个 REST 操作:
index、show、new、create、edit、update、destroy - 需要新行为? 创建一个新控制器,而不是自定义操作
- 操作长度:最多 1-5 行
- 空操作也没问题:让 Rails 按约定处理渲染
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 关键字下一级:
private
def set_message
@message = Message.find(params[:id])
end
def message_params
params.require(:message).permit(:body)
end
模型设计(胖模型)
模型负责业务逻辑、授权和广播:
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 属性
使用 Current 存储请求上下文,不要到处传递 current_user:
class Current < ActiveSupport::CurrentAttributes
attribute :user, :session
end
# Usage anywhere in app
Current.user.can_administer?(@message)
Ruby 语法偏好
DHH 特有的风格(通用 Ruby 风格请参阅 ruby-coder skill):
# Symbol arrays with spaces inside brackets
before_action :set_message, only: %i[ show edit update destroy ]
# Expression-less case for cleaner conditionals
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
查询优化
优先使用 pluck(:name),而不是 map(&:name);使用 messages.count,而不是 messages.to_a.count —— 将工作交给数据库处理。
用于谓词的 StringInquirer
在字符串枚举上使用 .inquiry,让条件判断更易读:
class Event < ApplicationRecord
def action
super.inquiry
end
end
# Clean predicate methods
event.action.completed?
event.action.pending?
event.action.failed?
控制器响应模式
对于无响应正文的更新,使用 head :no_content;对于创建,使用 head :created。使用 bang 方法(create!、update!)以便快速失败。
当前用户资源的 My:: 命名空间
对于作用域限定到 Current.user 的资源,使用 My:: 命名空间:
# routes.rb
namespace :my do
resource :profile, only: %i[ show edit update ]
resources :notifications, only: %i[ index destroy ]
end
# app/controllers/my/profiles_controller.rb
class My::ProfilesController < ApplicationController
def show
@profile = Current.user
end
end
不需要带 ID 的 index 或 show——资源由 Current.user 隐式确定。
写入时计算
在保存期间执行数据处理,而不是在呈现期间执行:
# WRONG: Compute on read
def display_name
"#{first_name} #{last_name}".titleize
end
# CORRECT: Compute on write
before_save :set_display_name
private
def set_display_name
self.display_name = "#{first_name} #{last_name}".titleize
end
优点:支持分页和缓存,并降低视图的复杂度。
使用 Delegate 实现延迟加载
使用 delegate 通过关联实现延迟加载:
class Message < ApplicationRecord
belongs_to :session
delegate :user, to: :session
end
# Lazy loads user through session
message.user
命名约定
| 元素 | 约定 | 示例 |
|---|---|---|
| Setter 方法 | 使用 set_ 前缀 | set_message、set_room |
| 参数方法 | {model}_params | message_params |
| 关联名称 | 语义明确,而非泛化 | creator 而不是 user |
| Scope | 可链式调用且描述清晰 | with_creator、page_before |
| 谓词 | 以 ? 结尾 | direct?、can_administer? |
| 当前用户资源 | My:: 命名空间 | My::ProfilesController |
Hotwire/Turbo 模式
广播由模型负责:
# In model
def broadcast_create
broadcast_append_to room, :messages, target: "messages"
end
有关详细的 Hotwire 模式,请使用 hotwire-coder skill。
错误处理
捕获特定异常,并使用 bang 方法快速失败:
def create
@message = @room.messages.create_with_attachment!(message_params)
@message.broadcast_create
rescue ActiveRecord::RecordNotFound
render action: :room_not_found
end
将状态表示为记录(而非布尔值)
通过数据库记录而不是布尔列跟踪状态:
# WRONG: Boolean columns for state
class Card < ApplicationRecord
# closed: boolean, gilded: boolean columns
end
card.update!(closed: true)
card.closed? # Loses who/when/why
# CORRECT: State as separate records
class Card < ApplicationRecord
has_one :closure
has_one :gilding
def close(by:)
create_closure!(closed_by: by)
end
def closed?
closure.present?
end
end
card.close(by: Current.user)
card.closure.closed_by # Full audit trail
REST URL 转换
将自定义操作映射到嵌套资源控制器:
| 自定义操作 | REST 资源 |
|---|---|
POST /cards/:id/close | POST /cards/:id/closure |
DELETE /cards/:id/close | DELETE /cards/:id/closure |
POST /cards/:id/gild | POST /cards/:id/gilding |
POST /posts/:id/publish | POST /posts/:id/publication |
DELETE /posts/:id/publish | DELETE /posts/:id/publication |
# routes.rb
resources :cards do
resource :closure, only: %i[ create destroy ]
resource :gilding, only: %i[ create destroy ]
end
# app/controllers/cards/closures_controller.rb
class Cards::ClosuresController < ApplicationController
def create
@card = Card.find(params[:card_id])
@card.close(by: Current.user)
end
def destroy
@card = Card.find(params[:card_id])
@card.closure.destroy!
end
end
架构偏好
| 传统做法 | DHH 的做法 |
|---|---|
| PostgreSQL | SQLite(用于单租户) |
| Redis + Sidekiq | Solid Queue |
| Redis 缓存 | Solid Cache |
| Kubernetes | 单个 Docker 容器 |
| 服务对象 | 胖模型 |
| 策略对象(Pundit) | 在 User 模型中进行授权 |
| FactoryBot | Fixtures |
| 布尔状态列 | 将状态表示为记录 |
详细参考
如需全面的模式和示例,请参阅:
核心模式
references/patterns.md- 完整的代码模式及说明references/palkan-patterns.md- 命名空间模型类、计数器缓存、模型组织顺序、PostgreSQL 枚举references/concerns-organization.md- 模型专属与通用 concern、门面模式references/delegated-types.md- 避免 STI 问题的多态实现references/recording-pattern.md- 统一不同内容类型的抽象references/filter-objects.md- PORO 筛选器对象、基于 URL 的状态、可测试的查询构建references/database-patterns.md- UUIDv7、硬删除、以记录表示状态、计数器缓存、索引
Rails 组件
references/activerecord-tips.md- ActiveRecord 查询模式、验证、关联references/controllers-tips.md- 控制器模式、路由、速率限制、表单对象references/activestorage-tips.md- 文件上传、附件、Blob 处理
Hotwire
references/hotwire-tips.md- Turbo Frames、Turbo Streams、ViewComponentsreferences/turbo-morphing.md- Turbo 8 页面刷新与 morphing 模式references/stimulus-catalog.md- 可直接复制粘贴的 Stimulus 控制器(剪贴板、对话框、快捷键等)- 另请参阅:
hotwire-coder、stimulus-coder、viewcomponent-coder技能,了解详细模式
前端
references/css-architecture.md- 原生 CSS 模式(层、OKLCH、嵌套、暗色模式)
身份验证与多租户
references/passwordless-auth.md- 魔法链接身份验证、会话、身份模型references/multi-tenancy.md- 基于路径的租户划分、Cookie 作用域、支持租户的任务
基础设施与集成
references/webhooks.md- 安全的 Webhook 投递、SSRF 防护、重试策略references/caching-strategies.md- Russian Doll 缓存、Solid Cache、缓存分析references/config-tips.md- 配置、日志记录、部署模式references/structured-events.md- 用于结构化可观测性的 Rails 8.1Rails.eventAPIreferences/resources.md- 源材料链接及延伸阅读
理念概要
- REST 纯粹性:仅使用 7 个操作;为不同变体创建新控制器
- 厚模型:在模型中处理授权、广播和业务逻辑
- 薄控制器:每个操作 1-5 行;提取复杂逻辑
- 约定优于配置:空方法、隐式渲染
- 最少抽象:简单场景不使用服务对象
- 当前属性:在所有位置使用线程本地请求上下文
- Hotwire 优先:模型级广播、Turbo Streams、Stimulus
- 代码易读:语义化命名、小型方法,无需注释
成功指标
符合 DHH 风格的代码应满足:
- 控制器将 CRUD 动词映射到资源(没有自定义操作)
- 模型使用 concern 共享横向行为
- 状态使用记录而非布尔列
- 抽象保持精简(没有不必要的服务对象)
- 由数据库提供解决方案(使用 Solid Queue/Cache,而非 Redis)
- 所有交互均由 Turbo/Stimulus 处理
- 授权逻辑位于 User 模型中(
can_*?方法) - Current 属性提供请求上下文
- Scope 遵循命名约定(
chronologically、with_*等) - 提取属性时优先使用
pluck,而非map - 当前用户的资源使用
My::命名空间 - 在写入时计算数据,而非在展示时计算