Communitygithub.com

Rails 模式

適用於 Rails 7.1+ 及 8.x 應用程式的 Ruby on Rails 框架模式。涵蓋目錄約定、精簡控制器搭配服務物件、表單物件、查詢物件、慣用 ActiveRecord、背景任務、ViewComponent、Hotwire 及 Rails 8 Solid 技術棧。在建立或審查 Rails 應用程式、控制器、模型、服務、任務或檢視時使用。

Rails 模式 是什麼?

這項技能為現代 Ruby on Rails 應用程式(Rails 7.1+ 及 8.x)提供了一套經過社群驗證的框架模式,旨在幫助開發者建立可長期維護的應用程式,即使模型數量超過 50 個也能保持程式碼品質。主要支援 Claude Code、Cursor 等 AI 編碼代理平台,並透過詳細的文件和程式碼範例,指導開發者在 Rails 專案中應用精簡控制器、服務物件、表單物件、查詢物件、ViewComponent、Hotwire(Turbo 和 Stimulus)以及 Rails 8 的 Solid 技術棧(Solid Queue、Solid Cache、Solid Cable)。技能的核心是『如何做』,並與專注於『做什麼』及『何時做』的 Ruby 模式規則互補。透過結構化的目錄約定、可組合的查詢物件、具有結果物件的服務層級事務管理,以及明確的 Active Record 回呼與 Concern 使用指南,這項技能幫助開發者避免常見的反模式,例如在控制器或模型中混入業務邏輯。對於背景任務,它強調冪等性、傳遞記錄 ID 而非完整物件,並使用唯一索引來防止重複執行。對於前端,它推薦 Hotwire 用於伺服器渲染的應用程式,並在需要複雜交互相對性時才考慮 React/Vue。

相容平台~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),同時能清晰地組合記錄。

查詢物件

對於在控制器或服務之間重複使用的 ActiveRecord 查詢,或過於複雜而無法寫成 scope 的查詢,請提取一個查詢物件。該物件接受一個 scope 作為輸入,以便它可以被組合。經驗法則:一個 scope 如果增長到超過三個鏈結條件,或開始接受參數,就該變成一個查詢物件。

背景任務

將任何耗時的操作卸載到背景執行。(根據 Ruby 模式規則,對於吞吐量適中的全新 Rails 8 專案,使用 Solid Queue;當你需要成熟的監控能力、高吞吐量或現有的 Redis 時,則使用 Sidekiq。)無論使用哪種適配器:傳遞 ID 而非記錄本身、讓 perform 方法具有冪等性,並明確設定 retry_on 和 discard_on。

使用 ViewComponent 取代部分模板

對於具有條件渲染、超過兩個參數或在超過三個地方重複使用的檢視邏輯,建議使用 ViewComponent。Component 可以獨立測試,並明確展示其介面;而帶有深層條件邏輯的部分模板會成為技術債。

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 會在 create_or_find_by! 呼叫內解決衝突,回傳現有的資料列。這不需要任何任務層級的重試——上面的 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:點擊「編輯」只會替換這個 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 引入這個 Concern。它並非橫切關注點;它是 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/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.

相關技能