Communitygithub.com

rails-patterns

Ruby on Rails 框架模式,适用于 Rails 7.1+ 和 8.x 应用。涵盖目录约定、瘦控制器与服务对象、表单对象、查询对象、惯用 ActiveRecord、后台任务、ViewComponent、Hotwire 以及 Rails 8 Solid 技术栈。在构建或审查 Rails 应用的控制器、模型、服务、任务或视图时使用。

rails-patterns 是什么?

该技能为现代 Ruby on Rails 应用(Rails 7.1+ 和 8.x)提供了一套成熟的框架模式。Rails 本身是约定大于配置的,但社区在构建可维护性超过 50 个模型的应用时,已经收敛到了一些特定的模式。这个技能是“怎么做”的指南,涵盖了从目录结构、瘦控制器、服务对象、表单对象、查询对象到后台任务、ViewComponent、Hotwire 以及 Rails 8 的 Solid 技术栈(Solid Queue、Solid Cache、Solid Cable)的完整实践。它适用于各种场景,包括构建全栈或 API 应用、审查涉及 app/ 或 config/ 的 PR、生成模型/控制器/服务/任务,或者当控制器动作超过约 10 行、模型文件超过约 200 行、ActiveRecord 查询开始出现在控制器或视图中时。该技能的核心原则包括:遵循可预测的目录契约、通过服务对象提取业务逻辑以保持控制器瘦身、使用表单对象处理跨模型的复杂表单、使用查询对象封装可复用的复杂查询、确保后台任务的幂等性、优先使用 ViewComponent 而非部分视图来处理需要测试的视图逻辑、以及采用 Hotwire 作为默认的前端方案。此外,它还特别强调了 Rails 8 中基于数据库的后台任务、缓存和 ActionCable 解决方案,作为传统 Redis 方案的替代选择。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/rails-patterns

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Rails 模式

现代 Ruby on Rails 应用(Rails 7.1+ 和 8.x)的框架模式。Rails 本身就是约定大于配置的;这些是社区在构建可维护性超过 50 个模型的应用时收敛到的一些模式。此技能是关于“怎么做”。有关“做什么”和“何时做”(即选择哪种模式的决策),请参阅 Ruby 模式规则 —— 本仓库中的 rules/ruby/patterns.md,作为 rules/ecc/ruby/patterns.md 安装。

何时激活

  • 构建一个 Rails 应用(全栈、仅 API 或混合模式)
  • 审查涉及 app/ 或 config/ 的 PR
  • 生成模型、控制器、服务或任务
  • 控制器动作增长到约 10 行以上
  • 模型文件增长到约 200 行以上
  • ActiveRecord 查询开始在控制器或视图中出现

核心概念

目录契约

Rails 应用遵循可预测的结构。添加目录要深思熟虑,而非随意。

app/
  models/         ActiveRecord 模型。持久化和领域逻辑紧密围绕数据。
  controllers/    HTTP 请求处理。仅限薄弱的编排。
  views/          ERB 模板。不包含业务逻辑。
  components/     ViewComponent 类。需要测试的视图逻辑。
  services/       服务对象。多步骤的业务操作。
  forms/          表单对象。跨多个模型的复杂表单处理。
  queries/        查询对象。可复用、可组合的 ActiveRecord 查询。
  jobs/           后台任务。通过 Solid Queue、Sidekiq 或 GoodJob 执行的异步工作。
  mailers/        ActionMailer 类。
  helpers/        视图辅助方法。仅限于微小的展示逻辑。
  policies/       授权策略(如果使用 Pundit)。可选。
  channels/       用于 WebSocket 的 ActionCable 通道。

避免使用 app/lib/、app/utils/、app/managers/。如果某个功能不适合上述目录,通常意味着设计需要重新考虑,而不是需要一个新目录。真正通用的代码放在 lib/ 中。

瘦控制器

控制器接收请求,委托给正确的对象,并渲染响应。业务逻辑存在于其他地方。(根据 Ruby 模式规则,当控制器开始承担多个职责时,提取到服务对象。)

服务对象

处理涉及多个模型保存的业务操作的默认选择。保持一致的约定:

  • 按领域命名空间(Invoices::Create),而不是按后缀命名(InvoiceCreator)。
  • 类方法 .call 委托给实例方法 #call。
  • 返回一个 Result 对象,而不是布尔值或裸记录,以便调用者可以根据成功、错误和受影响的记录进行分支处理。
  • 将多记录写入包装在事务中。
  • 保持每个服务单一用途(Invoices::Create、Invoices::MarkPaid),永远不要用 Invoices::Manager。

表单对象

当表单跨越多个模型或包含不映射到列的字段时,使用表单对象而不是嵌套属性或在错误模型上的虚拟属性。它对视图来说就像一个模型(form_with model: @form),同时能干净地组合记录。

查询对象

对于在控制器或服务之间复用的 ActiveRecord 查询,或者对于复杂到无法使用 scope 的查询,提取一个查询对象,它接受一个 scope 作为输入以便组合。经验法则:一个 scope 增长到超过三个链式条件或开始接受参数时,就应该变成一个查询对象。

后台任务

将任何慢速操作卸载到后台。(根据 Ruby 模式规则,对于吞吐量适中的新建 Rails 8 项目使用 Solid Queue;当你需要成熟的观察能力、高吞吐量或现有 Redis 时使用 Sidekiq。)无论采用何种适配器:传递 ID 而非记录,使 perform 幂等,并显式设置 retry_on/discard_on。

ViewComponent 优于部分视图

对于具有条件渲染、两个以上参数或在超过三个地方复用的视图逻辑,优先使用 ViewComponent。组件可以独立测试并显式暴露其接口;而带有深层条件逻辑的部分视图会成为技术债务。

Hotwire:Turbo 和 Stimulus

默认的 Rails 前端技术栈。(根据 Ruby 模式规则,对于服务器渲染的应用,优先使用 Hotwire;只有当交互复杂性证明服务于前端的合理性时,才考虑使用 React/Vue。)Turbo Frames 用于部分页面更新,Turbo Streams 用于服务器驱动的更新,Stimulus 用于标记旁边的小型客户端行为。

Rails 8 Solid 技术栈

Rails 8 提供了基于数据库的默认组件,以前这些需要 Redis:Solid Queue(任务)、Solid Cache(缓存)、Solid Cable(ActionCable)。其权衡是增加数据库负载以换取减少一个基础设施组件;适合吞吐量适中的场景,而在高规模下 Redis 仍然胜出。Kamal 是默认的基于 Docker 的部署工具。

代码示例

使用服务对象的瘦控制器

# 不好:控制器中存在业务逻辑
class InvoicesController < ApplicationController
  def create
    @invoice = Invoice.new(invoice_params)
    @invoice.user = current_user
    @invoice.line_items.build(invoice_params[:line_items])
    @invoice.tax_total = TaxCalculator.new(@invoice).calculate
    @invoice.total = @invoice.line_items.sum(&:amount) + @invoice.tax_total

    if @invoice.save
      InvoiceMailer.created(@invoice).deliver_later
      AccountingExportJob.perform_later(@invoice.id)
      redirect_to @invoice, notice: "Invoice created"
    else
      render :new
    end
  end
end

# 好:控制器编排,服务完成工作
class InvoicesController < ApplicationController
  def create
    result = Invoices::Create.call(params: invoice_params, user: current_user)

    if result.success?
      redirect_to result.invoice, notice: "Invoice created"
    else
      @invoice = result.invoice
      render :new, status: :unprocessable_entity
    end
  end
end

服务对象

# app/services/invoices/create.rb
module Invoices
  class Create
    # Struct 使其在 Rails 7.1 支持的每个 Ruby 上都能运行。
    # 在 Ruby 3.2+ 上,`Data.define(:success?, :invoice, :errors)` 是一个更简洁的不可变替代方案。
    Result = Struct.new(:success, :invoice, :errors, keyword_init: true) do
      def success?
        success
      end
    end

    def self.call(params:, user:)
      new(params: params, user: user).call
    end

    def initialize(params:, user:)
      @params = params
      @user = user
    end

    def call
      invoice = build_invoice
      ApplicationRecord.transaction do
        invoice.save!
      end
      begin
        send_notifications(invoice)
      rescue StandardError => e
        Rails.logger.error("Notification dispatch failed for invoice #{invoice.id}: #{e.message}")
      end
      Result.new(success: true, invoice: invoice, errors: nil)
    rescue ActiveRecord::RecordInvalid => e
      Result.new(success: false, invoice: e.record, errors: e.record.errors)
    end

    private

    attr_reader :params, :user

    def build_invoice
      invoice = user.invoices.new(params.except(:line_items))
      invoice.line_items.build(params[:line_items])
      invoice.tax_total = TaxCalculator.call(invoice)
      invoice.total = invoice.line_items.sum(&:amount) + invoice.tax_total
      invoice
    end

    def send_notifications(invoice)
      InvoiceMailer.created(invoice).deliver_later
      AccountingExportJob.perform_later(invoice.id)
    end
  end
end

表单对象

# app/forms/signup_form.rb
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :email, :string
  attribute :password, :string
  attribute :company_name, :string
  attribute :terms_accepted, :boolean

  validates :email, presence: true, format: URI::MailTo::EMAIL_REGEXP
  validates :password, presence: true, length: { minimum: 12 }
  validates :company_name, presence: true
  validates :terms_accepted, acceptance: true

  attr_reader :user, :company

  def save
    return false unless valid?

    ApplicationRecord.transaction do
      @company = Company.create!(name: company_name)
      @user = @company.users.create!(email: email, password: password, role: :owner)
    end
    true
  rescue ActiveRecord::RecordInvalid => e
    errors.merge!(e.record.errors)
    false
  end
end

查询对象

# app/queries/invoices/overdue.rb
module Invoices
  class Overdue
    def self.call(scope: Invoice.all, as_of: Time.current)
      new(scope: scope, as_of: as_of).call
    end

    def initialize(scope:, as_of:)
      @scope = scope
      @as_of = as_of
    end

    def call
      scope
        .where(status: :sent)
        .where(due_date: ..as_of)
        .where.not(id: paid_invoice_ids)
        .includes(:customer, :line_items)
    end

    private

    attr_reader :scope, :as_of

    def paid_invoice_ids
      Payment.where(created_at: ..as_of).pluck(:invoice_id)
    end
  end
end

查询对象接受一个 scope,因此它们可以组合:Invoices::Overdue.call(scope: current_user.invoices)。

N+1 预防

# 不好:当视图调用 post.author.name 时出现 N+1
@posts = Post.published

# 好:预加载
@posts = Post.published.includes(:author)

includes 让 Rails 选择预加载还是 eager_load。强制使用 preload 进行独立查询,使用 eager_load 在基于关联过滤时进行 JOIN。从 Rails 6.1 开始,strict_loading 会在意外的懒加载时抛出异常。

计数器缓存

class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true
end
add_column :posts, :comments_count, :integer, default: 0, null: false

post.comments_count 变成了列读取,而不是 COUNT(*)。此示例假设是一个新表;向已有行的表添加计数器缓存需要回填,这超出了本文范围。

后台任务的结构

传递记录 ID,而不是记录。重试机制使传递具有至少一次语义,因此任何调用外部服务的任务都必须是幂等的——否则,在远程调用成功后发生瞬时故障会在下一次尝试时重复执行。

class AccountingExportJob < ApplicationJob
  queue_as :exports

  retry_on AccountingApi::TransientError, wait: :polynomially_longer, attempts: 5
  discard_on AccountingApi::PermanentError

  def perform(invoice_id)
    invoice = Invoice.find(invoice_id)
    export = AccountingExport.create_or_find_by!(
      invoice: invoice,
      idempotency_key: "invoice-export-#{invoice.id}-#{invoice.updated_at.to_i}"
    )
    return if export.completed_at?

    receipt = AccountingApi.export(invoice, idempotency_key: export.idempotency_key)
    export.update!(completed_at: Time.current, external_id: receipt.id)
  end
end
add_index :accounting_exports, :idempotency_key, unique: true

唯一索引是保证安全的关键:当两次尝试竞争时,数据库拒绝第二次插入,Active Record 在 call 内部解决冲突,返回现有行。这发生在没有任何任务级重试的情况下——上面的 retry_on 仅覆盖 AccountingApi::TransientError。守卫条件覆盖了远程调用之前的时间窗口;将 idempotency_key 传递给 API 覆盖了远程调用之后的时间窗口,因此 API 调用与 update! 之间的崩溃仍然能解析为单个导出。

ViewComponent

# app/components/invoice_status_badge_component.rb
class InvoiceStatusBadgeComponent < ViewComponent::Base
  STATUS_CLASSES = {
    draft: "bg-gray-100 text-gray-800",
    sent: "bg-blue-100 text-blue-800",
    paid: "bg-green-100 text-green-800",
    overdue: "bg-red-100 text-red-800"
  }.freeze

  def initialize(invoice:)
    @invoice = invoice
  end

  def call
    tag.span(@invoice.status.humanize, class: "rounded-full px-2 py-1 text-sm #{status_class}")
  end

  private

  def status_class
    STATUS_CLASSES.fetch(@invoice.status.to_sym, "bg-gray-100")
  end
end
<%= render InvoiceStatusBadgeComponent.new(invoice: @invoice) %>

Hotwire

<%# Turbo Frame: 点击编辑仅替换此框架 %>
<%= turbo_frame_tag "invoice_#{@invoice.id}" do %>
  <div class="invoice">
    <%= link_to "Edit", edit_invoice_path(@invoice) %>
  </div>
<% end %>
<%# Turbo Stream: app/views/comments/create.turbo_stream.erb %>
<%= turbo_stream.append "comments", @comment %>
<%= turbo_stream.update "comment_form", partial: "form", locals: { comment: Comment.new } %>
// app/javascript/controllers/copy_to_clipboard_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["source"]

  copy() {
    navigator.clipboard.writeText(this.sourceTarget.value)
  }
}

可接受与不可接受的回调

# 可接受:纯数据规范化
class User < ApplicationRecord
  before_validation :normalize_email

  private

  def normalize_email
    self.email = email.to_s.downcase.strip
  end
end

# 改为使用服务:隐藏在回调中的副作用
# class User < ApplicationRecord
#   after_create :send_welcome_email  # 难以选择退出,难以测试
# end

好的 Concern 与坏的 Concern

# 好:真正的横切关注点,可在不相关的模型间复用
# app/models/concerns/soft_deletable.rb
module SoftDeletable
  extend ActiveSupport::Concern

  included do
    scope :active, -> { where(deleted_at: nil) }
    scope :deleted, -> { where.not(deleted_at: nil) }
  end

  def soft_delete! = update!(deleted_at: Time.current)
  def restore! = update!(deleted_at: nil)
end

# 坏:一个“Concern”仅被一个模型使用,包含属于该模型自身的逻辑
# app/models/concerns/invoice_calculations.rb
module InvoiceCalculations
  extend ActiveSupport::Concern

  def calculate_total
    line_items.sum(&:amount) + tax_total
  end
end
# 只有 Invoice 包含它。它不是横切关注点;它是 Invoice 自身的逻辑,被隐藏在了模块中,只是为了看起来模型“瘦弱”。请把它放回 Invoice 中。

一个仅被一个类使用的 Concern 只是移动代码;它应该属于那个类。一个 Concern 应该封装一个跨多个模型的、可复用的行为。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

相关技能