返回 Skill 列表
extension
分类: 开发与工程无需 API Key

dhh-ruby-style

用DHH特有的37signals风格编写Ruby和Rails代码。在编写Ruby代码、Rails应用程序、创建模型、控制器或任何Ruby文件时使用此技能。当生成或重构Ruby/Rails代码、进行代码审查,或者用户提到DHH、37signals、Basecamp、HEY或Campfire风格时触发。体现REST的纯粹性、胖模型瘦控制器、当前属性、Hotwire模式以及“清晰胜于巧妙”的理念。

person作者: jakexiaohubgithub

DHH Ruby/Rails Style Guide

Write Ruby and Rails code following DHH's philosophy: clarity over cleverness, convention over configuration, developer happiness above all.

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
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:

  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:

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

Use Current for request context, never pass current_user everywhere:

class Current < ActiveSupport::CurrentAttributes
  attribute :user, :session
end

# Usage anywhere in app
Current.user.can_administer?(@message)

Ruby Syntax Preferences

# Symbol arrays with spaces inside brackets
before_action :set_message, only: %i[ show edit update destroy ]

# Modern hash syntax exclusively
params.require(:message).permit(:body, :attachment)

# Single-line blocks with braces
users.each { |user| user.notify }

# Ternaries for simple conditionals
@room.direct? ? @room.users : @message.mentionees

# Bang methods for fail-fast
@message = Message.create!(params)
@message.update!(message_params)

# Predicate methods with question marks
@room.direct?
user.can_administer?(@message)
@messages.any?

# 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

Naming Conventions

| Element | Convention | Example | |---------|------------|---------| | Setter methods | set_ prefix | set_message, set_room | | Parameter methods | {model}_params | message_params | | Association names | Semantic, not generic | creator not user | | Scopes | Chainable, descriptive | with_creator, page_before | | Predicates | End with ? | direct?, can_administer? |

Hotwire/Turbo Patterns

Broadcasting is model responsibility:

# In model
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 }

Error Handling

Rescue specific exceptions, fail fast with bang methods:

def create
  @message = @room.messages.create_with_attachment!(message_params)
  @message.broadcast_create
rescue ActiveRecord::RecordNotFound
  render action: :room_not_found
end

Architecture Preferences

| Traditional | DHH Way | |-------------|---------| | PostgreSQL | SQLite (for single-tenant) | | Redis + Sidekiq | Solid Queue | | Redis cache | Solid Cache | | Kubernetes | Single Docker container | | Service objects | Fat models | | Policy objects (Pundit) | Authorization on User model | | 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

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