Communitygithub.com

rails-patterns

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

rails-patterns 是什么?

此技能为基于 Ruby on Rails 7.1+ 和 8.x 的现代 Web 应用提供了一套经过社区验证的架构模式和最佳实践。它针对 Rails 应用超过 50 个模型后仍能保持可维护性的常见痛点,定义了从目录结构、控制器、服务对象、表单对象、查询对象到后台任务、ViewComponent 和 Hotwire 等一系列具体的编码约定和模式。该技能详细解释了每种模式的适用场景(例如,当控制器超过 10 行时使用服务对象,查询条件超过三个时提取为查询对象),并提供了可运行的代码示例,包括如何构建结构化的服务对象、表单对象和查询对象,以及如何通过 ViewComponent 替换部分视图。它还特别强调了两种常见的代码异味:跨切面但滥用的问题(如不必要的 Concern)和隐藏在回调中的副作用。技能文档支持代理(如 Claude Code、Cursor、Codex)在生成代码或提供审查意见时,能够输出符合 Rails 社区最新标准、易于测试和维护的代码。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/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),同时清晰地组合记录。

查询对象

对于在控制器或服务中重复使用,或过于复杂而无法使用 scope 的 ActiveRecord 查询,提取一个查询对象,它接受一个 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: "发票已创建"
    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: "发票已创建"
    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("通知派发失败,发票 #{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 选择 preload 还是 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 在调用内部解决冲突,返回现有行。这在没有任何任务级重试的情况下发生——上面的 retry_on 仅覆盖 AccountingApi::TransientError。return if export.completed_at? 守卫覆盖了远程调用前的时间窗口;通过将 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_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 应该被至少两个不相关的模型使用才有价值。在 app/models/concerns/ 中,如果不是为跨多个模型共享,就要质疑它。

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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能