Agent Skillsdilolabs/nosia › concern-patterns

concern-patterns

GitHub

指导在 Rails 中创建和重构 Model/Controller Concerns,用于提取跨模型或控制器的共享行为,实现代码复用与模块化组织。

.vibe/skills/concern-patterns/SKILL.md dilolabs/nosia

Trigger Scenarios

提取跨模型的重复关联、状态模式或作用域逻辑 重构控制器以提取公共父资源加载等横向关注点 用户提及 concerns、mixins 或模块化的需求

Install

npx skills add dilolabs/nosia --skill concern-patterns -g -y
More Options

Non-standard path

npx skills add https://github.com/dilolabs/nosia/tree/main/.vibe/skills/concern-patterns -g -y

Use without installing

npx skills use dilolabs/nosia@concern-patterns

指定 Agent (Claude Code)

npx skills add dilolabs/nosia --skill concern-patterns -a claude-code -g -y

安装 repo 全部 skill

npx skills add dilolabs/nosia --all -g -y

预览 repo 内 skill

npx skills add dilolabs/nosia --list

SKILL.md

Frontmatter
{
    "name": "concern-patterns",
    "license": "MIT",
    "description": "Creates and refactors model and controller concerns for shared behavior. Use when extracting shared code, organizing models with horizontal concerns, DRYing up controllers, or when user mentions concerns, mixins, or modules. WHEN NOT: Logic used by only one model (keep in place), service object extraction (use model-patterns), or job organization (use job-patterns).",
    "compatibility": "Ruby 3.3+, Rails 8.0+"
}

Concern Patterns (37signals)

Concerns for horizontal behavior, inheritance for vertical specialization.

Project knowledge

Tech Stack: Rails 8.2 (edge), ActiveSupport::Concern Location: app/models/[model]/ for model concerns, app/controllers/concerns/ for controller concerns

Commands:

ls app/models/concerns/                      # List shared concerns
ls app/models/card/                          # List Card concerns
bin/rails runner "puts Card.included_modules" # Check usage
bin/rails test test/models/                  # Run model tests

Core principles

Each concern should be:

  • Self-contained: All related code (associations, validations, scopes, methods) in one place
  • Cohesive: Focused on one aspect (e.g., Closeable, Watchable, Searchable)
  • Composable: Models include multiple concerns to build up behavior

When to extract a concern

Extract when you see:

  1. Repeated associations across models

    # Multiple models have:
    has_many :comments, as: :commentable
    # Extract to: app/models/concerns/commentable.rb
    
  2. Repeated state patterns

    # Multiple models have close/reopen pattern
    # Extract to: Card::Closeable, Board::Publishable, etc.
    
  3. Repeated scopes

    # Multiple models have:
    scope :recent, -> { order(created_at: :desc) }
    # Extract to: Timestampable concern
    
  4. Repeated controller patterns

    # Multiple controllers load parent resource
    # Extract to: ParentScoped concern
    

Do NOT extract when:

  • Code is used by only one model (YAGNI)
  • You'd create a god concern with unrelated methods
  • Logic should be in explicit model methods instead

Model concern structure

State management concern

# app/models/card/closeable.rb
module Card::Closeable
  extend ActiveSupport::Concern

  included do
    has_one :closure, dependent: :destroy

    scope :open, -> { where.missing(:closure) }
    scope :closed, -> { joins(:closure) }
  end

  def close(user: Current.user)
    create_closure!(user: user)
    track_event "card_closed", user: user
  end

  def reopen
    closure&.destroy!
    track_event "card_reopened"
  end

  def closed?
    closure.present?
  end

  def open?
    !closed?
  end

  def closed_at
    closure&.created_at
  end

  def closed_by
    closure&.user
  end
end

Association concern

# app/models/card/assignable.rb
module Card::Assignable
  extend ActiveSupport::Concern

  included do
    has_many :assignments, dependent: :destroy
    has_many :assignees, through: :assignments, source: :assignee

    scope :assigned_to, ->(users) { joins(:assignments).where(assignments: { assignee: users }).distinct }
    scope :unassigned, -> { where.missing(:assignments) }
  end

  def assign(user)
    assignments.create!(user: user) unless assigned_to?(user)
    track_event "card_assigned", user: user, particulars: { assignee_id: user.id }
  end

  def unassign(user)
    assignments.where(user: user).destroy_all
  end

  def assigned_to?(user)
    assignees.include?(user)
  end
end

Behavior concern with class methods

# app/models/card/searchable.rb
module Card::Searchable
  extend ActiveSupport::Concern

  included do
    scope :search, ->(query) { where("title LIKE ? OR body LIKE ?", "%#{query}%", "%#{query}%") }
  end

  class_methods do
    def search_with_ranking(query)
      search(query).order("search_rank DESC")
    end

    def top_results(query, limit: 10)
      search_with_ranking(query).limit(limit)
    end
  end
end

Controller concern structure

# app/controllers/concerns/card_scoped.rb
module CardScoped
  extend ActiveSupport::Concern

  included do
    before_action :set_card
    before_action :set_board
  end

  private

  def set_card
    @card = Current.account.cards.find(params[:card_id])
  end

  def set_board
    @board = @card.board
  end

  def render_card_replacement
    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: turbo_stream.replace(
          dom_id(@card, :card_container),
          partial: "cards/container",
          locals: { card: @card.reload }
        )
      end
      format.html { redirect_to @card }
    end
  end
end

Naming conventions

  • Model concerns (adjectives): Closeable, Publishable, Watchable, Assignable, Searchable, Eventable, Broadcastable, Readable, Positionable
  • Controller concerns (nouns): CardScoped, BoardScoped, FilterScoped, CurrentRequest, CurrentTimezone, Authentication

Testing concerns

Test in isolation:

# test/models/concerns/closeable_test.rb
class CloseableTest < ActiveSupport::TestCase
  setup do
    @card = cards(:logo)
  end

  test "close creates closure record" do
    assert_difference -> { Closure.count }, 1 do
      @card.close
    end
    assert @card.closed?
  end

  test "reopen destroys closure record" do
    @card.close
    assert_difference -> { Closure.count }, -1 do
      @card.reopen
    end
    assert @card.open?
  end

  test "closed scope finds closed records" do
    @card.close
    assert_includes Card.closed, @card
    refute_includes Card.open, @card
  end
end

Refactoring workflow

  1. Identify the pattern -- Find duplicated code across models/controllers
  2. Name the concern -- Use an adjective describing the capability
  3. Create the file -- app/models/[model]/[concern].rb or app/controllers/concerns/[concern].rb
  4. Move code -- Associations, validations, scopes, methods
  5. Include it -- Add include ConcernName to models/controllers
  6. Write tests -- Test concern in isolation and in context
  7. Remove duplication -- Delete the old code from models/controllers

Files to create

  1. Concern file: app/models/card/closeable.rb or app/controllers/concerns/card_scoped.rb
  2. Model/Controller: Add include ConcernName
  3. Test file: test/models/concerns/closeable_test.rb

See references/concern-catalog.md for the full catalog of concern types.

Boundaries

  • Always: Extract repeated code into concerns, keep concerns focused on one aspect, include all related code (associations, scopes, methods), write tests, use extend ActiveSupport::Concern, namespace model concerns under the model
  • Ask first: Before creating concerns that span multiple domains, before extracting concerns with complex dependencies, before modifying widely-used concerns
  • Never: Create god concerns with too many responsibilities, use concerns to hide service objects, skip included do block for callbacks/associations, create concerns for one-off code

Version History

  • edd76af Current 2026-08-20 14:25

Same Skill Collection

.claude/skills/api-patterns/SKILL.md
.claude/skills/auth-setup/SKILL.md
.claude/skills/caching-patterns/SKILL.md
.claude/skills/concern-patterns/SKILL.md
.claude/skills/crud-patterns/SKILL.md
.claude/skills/event-tracking/SKILL.md
.claude/skills/job-patterns/SKILL.md
.claude/skills/mailer-patterns/SKILL.md
.claude/skills/migration-patterns/SKILL.md
.claude/skills/model-patterns/SKILL.md
.claude/skills/multi-tenant-setup/SKILL.md
.claude/skills/state-records/SKILL.md
.claude/skills/stimulus-patterns/SKILL.md
.claude/skills/testing-patterns/SKILL.md
.claude/skills/turbo-patterns/SKILL.md
.opencode/skills/api-patterns/SKILL.md
.opencode/skills/auth-setup/SKILL.md
.opencode/skills/caching-patterns/SKILL.md
.opencode/skills/concern-patterns/SKILL.md
.opencode/skills/crud-patterns/SKILL.md
.opencode/skills/event-tracking/SKILL.md
.opencode/skills/job-patterns/SKILL.md
.opencode/skills/mailer-patterns/SKILL.md
.opencode/skills/migration-patterns/SKILL.md
.opencode/skills/model-patterns/SKILL.md
.opencode/skills/multi-tenant-setup/SKILL.md
.opencode/skills/state-records/SKILL.md
.opencode/skills/stimulus-patterns/SKILL.md
.opencode/skills/testing-patterns/SKILL.md
.opencode/skills/turbo-patterns/SKILL.md
.vibe/skills/api-patterns/SKILL.md
.vibe/skills/auth-setup/SKILL.md
.vibe/skills/caching-patterns/SKILL.md
.vibe/skills/crud-patterns/SKILL.md
.vibe/skills/event-tracking/SKILL.md
.vibe/skills/job-patterns/SKILL.md
.vibe/skills/mailer-patterns/SKILL.md
.vibe/skills/migration-patterns/SKILL.md
.vibe/skills/model-patterns/SKILL.md
.vibe/skills/multi-tenant-setup/SKILL.md
.vibe/skills/state-records/SKILL.md
.vibe/skills/stimulus-patterns/SKILL.md
.vibe/skills/testing-patterns/SKILL.md
.vibe/skills/turbo-patterns/SKILL.md

Metadata

Files
0
Version
edd76af
Hash
5b61f97a
Indexed
2026-08-20 14:25

- 위키
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-25 02:55
浙ICP备14020137号-1 $방문자$