关于 rails-concern
为 Rails 开发者提供以测试驱动方式创建 ActiveSupport::Concern 的流程,说明适用场景、实现步骤和常见模式。
主要功能
- 01先写测试再实现 concern
- 02指导模型与控制器 concern
- 03提供常见 concern 模式
- 04说明应避免抽象的情形
- 05检查单一职责与测试结果
适用场景
- 01为多个模型共享验证逻辑
- 02复用常见作用域或回调
- 03为控制器共享认证辅助逻辑
- 04通过测试驱动创建 concern
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ thibautbaissac/ rails_ai_agents --skill rails-concern安装后在 Claude Code 中输入/rails-concern确认已加载。
详细介绍
Rails Concern 生成器(TDD)
优先编写测试,为共享行为创建 concerns(ActiveSupport::Concern 模块)。
快速开始
- 编写用于测试 concern 行为的失败 spec
- 运行 spec,确认 RED
- 在
app/models/concerns/或app/controllers/concerns/中实现 concern - 运行 spec,确认 GREEN
何时使用 Concerns
适用场景:
- 在多个模型之间共享验证
- 多个模型使用的通用作用域
- 共享回调(例如,生成 UUID)
- 控制器身份验证/授权辅助方法
- 分页或筛选逻辑
以下情况应避免使用 concerns:
- 逻辑只在一个地方使用(YAGNI)
- 创建包含无关方法的“全能” concern
- 逻辑更适合放在服务对象中
TDD 工作流
步骤 1:创建 Concern Spec(RED)
对于模型 Concern,通过包含它的模型进行测试:
# spec/models/concerns/[concern_name]_spec.rb
RSpec.describe [ConcernName] do
# Create a test class that includes the concern
let(:test_class) do
Class.new(ApplicationRecord) do
self.table_name = "events" # Use existing table
include [ConcernName]
end
end
let(:instance) { test_class.new }
describe "included behavior" do
it "adds the expected methods" do
expect(instance).to respond_to(:method_from_concern)
end
end
describe "#method_from_concern" do
it "behaves as expected" do
expect(instance.method_from_concern).to eq(expected_value)
end
end
describe "class methods" do
it "adds scope" do
expect(test_class).to respond_to(:scope_name)
end
end
end
另一种方式:通过实际使用该 concern 的模型进行测试:
# spec/models/event_spec.rb
RSpec.describe Event, type: :model do
describe "[ConcernName] behavior" do
describe "#method_from_concern" do
let(:event) { build(:event) }
it "does something" do
expect(event.method_from_concern).to eq(expected)
end
end
end
end
对于控制器 Concern,通过请求 spec 进行测试:
# spec/requests/[feature]_spec.rb
RSpec.describe "[Feature]", type: :request do
describe "pagination (from Paginatable concern)" do
let(:user) { create(:user) }
before { sign_in user }
it "paginates results" do
create_list(:resource, 30, account: user.account)
get resources_path
expect(response.body).to include("page")
end
end
end
步骤 2:运行 Spec(确认 RED)
bundle exec rspec spec/models/concerns/[concern_name]_spec.rb
# OR
bundle exec rspec spec/models/[model]_spec.rb
步骤 3:实现 Concern(GREEN)
模型 Concern:
# app/models/concerns/[concern_name].rb
module [ConcernName]
extend ActiveSupport::Concern
included do
# Callbacks
before_validation :generate_uuid, on: :create
# Validations
validates :uuid, presence: true, uniqueness: true
# Scopes
scope :with_uuid, ->(uuid) { where(uuid: uuid) }
scope :recent, -> { order(created_at: :desc) }
end
# Class methods
class_methods do
def find_by_uuid!(uuid)
find_by!(uuid: uuid)
end
end
# Instance methods
def generate_uuid
self.uuid ||= SecureRandom.uuid
end
def short_uuid
uuid&.split("-")&.first
end
end
控制器 Concern:
# app/controllers/concerns/[concern_name].rb
module [ConcernName]
extend ActiveSupport::Concern
included do
before_action :set_locale
helper_method :current_locale
end
class_methods do
def skip_locale_for(*actions)
skip_before_action :set_locale, only: actions
end
end
private
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
def current_locale
I18n.locale
end
end
步骤 4:运行 Spec(确认 GREEN)
bundle exec rspec spec/models/concerns/[concern_name]_spec.rb
常见 Concern 模式
模式 1:UUID 生成
# app/models/concerns/has_uuid.rb
module HasUuid
extend ActiveSupport::Concern
included do
before_validation :generate_uuid, on: :create
validates :uuid, presence: true, uniqueness: true
end
private
def generate_uuid
self.uuid ||= SecureRandom.uuid
end
end
模式 2:软删除
# 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) }
default_scope { active }
end
def soft_delete
update(deleted_at: Time.current)
end
def restore
update(deleted_at: nil)
end
def deleted?
deleted_at.present?
end
end
模式 3:可搜索
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
class_methods do
def search(query)
return all if query.blank?
where("name ILIKE :q OR email ILIKE :q", q: "%#{query}%")
end
end
end
模式 4:可审计
# app/models/concerns/auditable.rb
module Auditable
extend ActiveSupport::Concern
included do
has_many :audit_logs, as: :auditable, dependent: :destroy
after_create :log_creation
after_update :log_update
end
private
def log_creation
audit_logs.create(action: "created", changes: attributes)
end
def log_update
return unless saved_changes.any?
audit_logs.create(action: "updated", changes: saved_changes)
end
end
模式 5:控制器可筛选
# app/controllers/concerns/filterable.rb
module Filterable
extend ActiveSupport::Concern
private
def apply_filters(scope, allowed_filters)
allowed_filters.each do |filter|
if params[filter].present?
scope = scope.where(filter => params[filter])
end
end
scope
end
end
用法
在模型中:
class Event < ApplicationRecord
include HasUuid
include SoftDeletable
include Searchable
end
在控制器中:
class ApplicationController < ActionController::Base
include Filterable
end
检查清单
- 首先编写 spec(RED)
- 使用
extend ActiveSupport::Concern - 使用
included块定义回调/验证/作用域 - 使用
class_methods块定义类方法 - 在块外定义实例方法
- 单一职责(每个 concern 只负责一个用途)
- 命名恰当(能说明它添加的功能)
- 所有 spec 均为 GREEN
评论
支持 Markdown