mcpField
D
dhh-coder

dhh-coder

github.com/majesticlabs-dev开发5067 次安装7 周前更新
代码仓库

关于 dhh-coder

dhh-coder 是一项面向 Ruby 和 Rails 开发者的编码风格技能,整理 DHH 风格的架构与实现建议。内容涵盖 REST 控制器、胖模型、命名、查询优化及 Hotwire 等模式。

主要功能

  1. 01遵循 REST 七项控制器动作
  2. 02将业务逻辑放入模型
  3. 03使用 Current 保存请求上下文
  4. 04优化数据库查询与数据写入
  5. 05提供 Rails、Hotwire 参考模式

适用场景

  1. 01按 DHH 风格编写 Rails 功能
  2. 02重构控制器与模型职责
  3. 03优化 ActiveRecord 查询
  4. 04设计状态记录与 REST 路由

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/majesticlabs-dev/majestic-marketplace --skill dhh-coder

安装后在 Claude Code 中输入/dhh-coder确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
activerecord-tips.md
activestorage-tips.md
caching-strategies.md
concerns-organization.md
config-tips.md
controllers-tips.md
css-architecture.md
database-patterns.md
delegated-types.md
filter-objects.md
hotwire-tips.md
multi-tenancy.md
palkan-patterns.md
passwordless-auth.md
patterns.md
recording-pattern.md
resources.md
stimulus-catalog.md
structured-events.md
turbo-morphing.md
webhooks.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}_paramsmessage_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/closePOST /cards/:id/closure
DELETE /cards/:id/closeDELETE /cards/:id/closure
POST /cards/:id/gildPOST /cards/:id/gilding
POST /posts/:id/publishPOST /posts/:id/publication
DELETE /posts/:id/publishDELETE /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 的做法
PostgreSQLSQLite(用于单租户)
Redis + SidekiqSolid Queue
Redis 缓存Solid Cache
Kubernetes单个 Docker 容器
服务对象胖模型
策略对象(Pundit)在 User 模型中进行授权
FactoryBotFixtures
布尔状态列将状态表示为记录

详细参考

如需全面的模式和示例,请参阅:

核心模式
  • 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、ViewComponents
  • references/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.1 Rails.event API
  • references/resources.md - 源材料链接及延伸阅读

理念概要

  1. REST 纯粹性:仅使用 7 个操作;为不同变体创建新控制器
  2. 厚模型:在模型中处理授权、广播和业务逻辑
  3. 薄控制器:每个操作 1-5 行;提取复杂逻辑
  4. 约定优于配置:空方法、隐式渲染
  5. 最少抽象:简单场景不使用服务对象
  6. 当前属性:在所有位置使用线程本地请求上下文
  7. Hotwire 优先:模型级广播、Turbo Streams、Stimulus
  8. 代码易读:语义化命名、小型方法,无需注释

成功指标

符合 DHH 风格的代码应满足:

  • 控制器将 CRUD 动词映射到资源(没有自定义操作)
  • 模型使用 concern 共享横向行为
  • 状态使用记录而非布尔列
  • 抽象保持精简(没有不必要的服务对象)
  • 由数据库提供解决方案(使用 Solid Queue/Cache,而非 Redis)
  • 所有交互均由 Turbo/Stimulus 处理
  • 授权逻辑位于 User 模型中(can_*? 方法)
  • Current 属性提供请求上下文
  • Scope 遵循命名约定(chronologically、with_* 等)
  • 提取属性时优先使用 pluck,而非 map
  • 当前用户的资源使用 My:: 命名空间
  • 在写入时计算数据,而非在展示时计算

评论

支持 Markdown