SuperAPI AI 工具集

rails-service-object

编程与开发

查看项目

更新于 10-16 04:08

技能介绍

用途

Rails Service Object 用于在 Rails 应用中封装业务逻辑,遵循单一职责原则:每个服务对象只有一个公共方法 #call,输入输出契约清晰,便于隔离测试,并可在控制器、后台任务(Job)、rake 任务之间复用。它适合把业务逻辑从控制器中抽取出来,避免控制器承担过多职责。

何时使用

资料给出的判断标准:

  • 复杂业务逻辑:使用服务对象
  • 多个模型交互:使用服务对象
  • 外部 API 调用:使用服务对象
  • 跨控制器共享逻辑:使用服务对象
  • 简单 CRUD 操作:不使用(放在模型中)
  • 单模型校验:不使用(放在模型中)

主要能力

  • 单一公共方法 #call,职责明确
  • 通过构造函数注入依赖,便于测试时替换
  • 统一返回 Result 对象,区分成功与失败
  • 错误被捕获并包装为 Result,而不是抛出异常
  • 提供完整的工作流清单,从定义契约到最终测试

使用方式

工作流清单

  1. 定义输入/输出契约
  2. 创建服务 spec(RED)
  3. 运行 spec(失败——服务不存在)
  4. 创建服务文件,包含空的 #call
  5. 运行 spec(失败——返回值错误)
  6. 实现 #call 方法
  7. 运行 spec(GREEN)
  8. 添加错误场景 spec
  9. 实现错误处理
  10. 最终运行 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 对象
  • 控制器中写业务逻辑:应抽取到服务

资料未提供该技能的安装命令、收费方式、支持平台或具体效果数据。