返回 Skill 列表
extension
分类: 开发与工程无需 API Key

rails-service-patterns

Rails服务对象模式和业务逻辑组织。当使用服务对象、提取业务逻辑、实现命令/查询模式或组织app/services时自动调用。在遇到“服务对象”、“服务”、“业务逻辑”、“工作流”、“编排”、“命令模式”、“查询对象”、“表单对象”、“交互器”、“结果对象”这些术语时触发。

person作者: jakexiaohubgithub

Rails Service Object Patterns

Analyze and recommend patterns for extracting and organizing business logic in Rails applications.

Quick Reference

| Pattern | Use When | Entry Point | |---------|----------|-------------| | Basic Service | Single operation with transaction | CreateOrder.new(...).call | | Result Object | Caller needs success/failure + data | Result.new(success?: true, data:) | | Form Object | Multi-model form submissions | RegistrationForm.new(params).save | | Query Object | Complex reusable queries | UserSearchQuery.new(scope).call | | Policy Object | Authorization decisions | PostPolicy.new(user, post).update? |

Supporting Documentation

  • patterns.md - Result objects, form objects, and profile-aware guidance

Core Principles

  1. VerbNoun naming: CreateOrder, SendInvitation -- never OrderService or UserManager
  2. One public method: Expose only call (or perform)
  3. Explicit return values: Use Result objects, never exceptions for expected flow control
  4. Profile-aware extraction: See "When to Extract" below

When to Extract a Service (Profile-Dependent)

| Scenario | Omakase | Service-Oriented / API-First | |----------|---------|------------------------------| | Logic on a single model's own data | Model method or concern | Model method | | Shared behavior across models | Concern | Concern | | Domain logic for one model | Concern | Service object | | Multi-model workflow with rollback | Model method + transaction | Service object | | External API call | Model method wrapping client | Service object | | Simple side effect (email, log) | Callback (after_commit) | Service object |

Omakase: Only extract to a service when the workflow genuinely spans multiple unrelated models or external systems. Prefer concerns and enriched model methods.

Service-oriented / API-first: Service objects are the default extraction target for any non-trivial business logic.

Result Object Pattern

Use Struct.new(keyword_init: true) for lightweight results. Never raise exceptions for expected failures (validation, auth, payment decline).

class AuthenticateUser
  Result = Struct.new(:success?, :user, :error, keyword_init: true)

  def initialize(email:, password:)
    @email = email
    @password = password
  end

  def call
    user = User.find_by(email: @email)
    if user&.authenticate(@password)
      Result.new(success?: true, user: user)
    else
      Result.new(success?: false, error: "Invalid credentials")
    end
  end
end

See patterns.md for the enhanced monad-like ServiceResult with on_success/on_failure chaining.

Anti-Patterns

| Anti-Pattern | Problem | Fix | |-------------|---------|-----| | God service (100+ lines) | Does too much | Split into composable services | | Raising exceptions for flow control | Expensive, hard to handle | Use Result objects | | Deep service-calls-service chains | Hidden coupling | Orchestrate from controller or coordinator | | self.call class method pattern | No instance state, limits DI | Use instance methods with constructor DI | | No return value | Caller can't react to failures | Always return Result or meaningful value | | Service modifying passed-in objects | Surprising side effects | Return new objects or be explicit | | VerbNoun naming violation (UserService) | Unclear responsibility, attracts god service | One service = one operation = one verb |

Output Format

When analyzing or creating services, provide:

  1. Service file in app/services/ with VerbNoun naming
  2. Result struct if callers need success/failure status
  3. Controller integration showing how to call and handle results
  4. Test outline covering happy path, failure cases, and edge cases
  5. Error handling strategy (Result objects for expected failures, exceptions for unexpected)