用途
Rails Service Object 用于在 Rails 应用中封装业务逻辑,遵循单一职责原则:每个服务对象只有一个公共方法 #call,输入输出契约清晰,便于隔离测试,并可在控制器、后台任务(Job)、rake 任务之间复用。它适合把业务逻辑从控制器中抽取出来,避免控制器承担过多职责。
何时使用
资料给出的判断标准:
- 复杂业务逻辑:使用服务对象
- 多个模型交互:使用服务对象
- 外部 API 调用:使用服务对象
- 跨控制器共享逻辑:使用服务对象
- 简单 CRUD 操作:不使用(放在模型中)
- 单模型校验:不使用(放在模型中)
主要能力
- 单一公共方法 #call,职责明确
- 通过构造函数注入依赖,便于测试时替换
- 统一返回 Result 对象,区分成功与失败
- 错误被捕获并包装为 Result,而不是抛出异常
- 提供完整的工作流清单,从定义契约到最终测试
使用方式
工作流清单
- 定义输入/输出契约
- 创建服务 spec(RED)
- 运行 spec(失败——服务不存在)
- 创建服务文件,包含空的 #call
- 运行 spec(失败——返回值错误)
- 实现 #call 方法
- 运行 spec(GREEN)
- 添加错误场景 spec
- 实现错误处理
- 最终运行 spec
定义契约
以 Orders::CreateService 为例,契约包含:
- 目的:创建新订单,包含库存校验和支付处理
- 输入:user(必填)、items(必填,数组,元素为 {product_id:, quantity:})、payment_method_id(可选)
- 输出(Result 对象):成功时 success? 为 true,data 为 Order 实例;失败时 success? 为 false,error 为字符串,code 为符号
- 依赖:inventory_service、payment_gateway
- 副作用:创建 Order 和 OrderItem 记录、扣减库存、扣款、异步发送确认邮件
服务 Spec
位置:spec/services/orders/create_service_spec.rb。使用 RSpec,通过 subject(:service) 和 subject(:result) 组织测试,覆盖有效输入、空 items、库存不足等场景。
实现服务
位置:app/services/orders/create_service.rb。构造函数通过关键字参数注入 inventory_service 和 payment_gateway,默认值分别为 InventoryService.new 和 PaymentGateway.new。call 方法接收 user:、items:、payment_method_id: 关键字参数,先校验 items 是否为空、库存是否充足,再在事务中创建订单并扣减库存,最后处理支付。使用 rescue 捕获 ActiveRecord::RecordInvalid 和 PaymentError,返回 failure。
Result 对象
使用 Ruby 3.2+ 的 Data.define 创建不可变结果对象:
# app/services/result.rb
# frozen_string_literal: true
Result = Data.define(:success, :data, :error) do
def success? = success
def failure? = !success
end
调用服务
从控制器调用:
class OrdersController < ApplicationController
def create
result = Orders::CreateService.new.call(
user: current_user,
items: order_params[:items],
payment_method_id: order_params[:payment_method_id]
)
if result.success?
render json: result.data, status: :created
else
render json: { error: result.error }, status: :unprocessable_entity
end
end
end
从 Job 调用:
class ProcessOrderJob < ApplicationJob
def perform(user_id, items)
user = User.find(user_id)
result = Orders::CreateService.new.call(user: user, items: items)
unless result.success?
Rails.logger.error("Order failed: #{result.error}")
# Handle failure (retry, notify, etc.)
end
end
end
使用 Mock 依赖进行测试
RSpec.describe Orders::CreateService do
let(:inventory_service) { instance_double(InventoryService) }
let(:payment_gateway) { instance_double(PaymentGateway) }
let(:service) { described_class.new(inventory_service: inventory_service, payment_gateway: payment_gateway) }
before do
allow(inventory_service).to receive(:available?).and_return(true)
allow(inventory_service).to receive(:decrement)
allow(payment_gateway).to receive(:charge)
end
# Tests...
end
目录结构
app/services/
├── result.rb # Shared Result class
├── application_service.rb # Optional base class
├── orders/
│ ├── create_service.rb
│ ├── cancel_service.rb
│ └── refund_service.rb
├── users/
│ ├── register_service.rb
│ └── update_profile_service.rb
└── payments/
├── charge_service.rb
└── refund_service.rb
约定
- 命名:VerbNounService(例如 CreateOrderService)
- 位置:app/services/[namespace]/[name]_service.rb
- 接口:单一公共方法 #call
- 返回:始终返回 Result 对象
- 依赖:通过构造函数注入
- 错误:捕获并包装,不要抛出
适用场景
- 从控制器中抽取复杂业务逻辑
- 涉及多个模型交互的操作
- 需要调用外部 API 的业务
- 多个控制器、Job 或 rake 任务共享的逻辑
限制与反模式
应避免的反模式:
- God service:承担过多职责
- 隐藏依赖:使用全局变量而不是注入
- 没有返回契约:返回不同类型
- 抛出异常:应使用 Result 对象
- 控制器中写业务逻辑:应抽取到服务
资料未提供该技能的安装命令、收费方式、支持平台或具体效果数据。