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 應用程式設計,幫助開發者保持程式碼的可維護性,即使在超過 50 個模型的大型專案中也能運作。它強制執行一個嚴格的「目錄合約」(app/models, app/services, app/forms, app/queries 等),並提供了精簡控制器、服務對象、表單對象和查詢對象的具體範例。技能也詳細說明了背景任務的冪等性實作(使用唯一索引和可重試/可丟棄策略)、ViewComponent 相較於部分模板的優勢,以及 Hotwire(Turbo Frames/Streams + Stimulus)的整合。對於 Rails 8,它特別介紹了 Solid 堆疊(Solid Queue, Solid Cache, Solid Cable)作為無 Redis 的替代方案。這個技能適用於 Cursor、Claude Code 等 AI 編碼代理,在生成或審查 Rails 程式碼時提供一致的實作指南,確保團隊遵循社群最佳實務。

相容平台~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 在 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。防護措施涵蓋了遠端呼叫之前的時間窗口;將 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.

相關技能