Published on

〜ControllerとModelが肥大化するの厳しいって〜Railsで複雑なロジックを整理するためのServiceオブジェクトパターンを実例付きで解説❗️

Authors

この記事は ex-crowdworks Advent Calendar 2024の16日目の記事です。

はじめに

今年、株式会社クラウドワークスを退職した@nisyuuです。今年、乾燥機付き洗濯機を使い始めて人生変わりました。 エンジニアとしてクラウドワークステック(旧クラウドテック)というフリーランスと企業をマッチングするエージェントサービスを開発していました。

Webアプリケーションを開発する中でモデルやコントローラにロジックが集中しすぎて複雑化してしまうケースがよくあります。 このような問題を解決するために役立つのが「Serviceオブジェクトパターン」です。 本記事では初心者向けにServiceオブジェクトパターンの概要とRailsにおける実装方法を解説します。

Serviceオブジェクトパターンとは?

Serviceオブジェクトパターンは特定のビジネスロジックやアプリケーション固有の操作を独立したクラスに切り出すデザインパターンの一つで、以下の特徴を持っています。

  • 単一責任 一つのクラスが一つの明確な目的を持つ
  • 再利用可能 共通のロジックをまとめて再利用性を高める
  • テストが容易 独立したロジックをテストしやすくなる

たとえばユーザーの登録処理や外部APIとの通信、複雑なクエリの実行などが該当します。

なぜServiceオブジェクトパターンを使うのか?

RailsのMVCアーキテクチャでは以下のような課題に直面することがあります:

  • コントローラの肥大化 複雑なロジックがコントローラに集中してしまう
  • モデルの肥大化 モデルに大量のメソッドやコールバックが追加されることによる可読性低下
  • ロジックの分散 アプリケーションロジックの散らばりで保守性が悪化

Serviceオブジェクトパターンを導入することでこれらの問題を解決できます。

Serviceオブジェクトパターンのメリットとデメリット

メリット

  1. 責務の明確化
    • コントローラやモデルから複雑なロジックを分離できるためコードの可読性が向上します
  2. 再利用性の向上
    • 複数の箇所で使用されるロジックを一元管理できるため重複コードを削減できます
  3. テストが容易
    • ロジックが独立したクラスにまとめられているため単体テストがしやすくなります
  4. 保守性の向上
    • コードの変更が局所化されるため、修正が容易になります

デメリット

  1. 過剰設計のリスク
    • 小規模なアプリケーションでは必要以上に構造を複雑化する可能性があります
  2. 学習コスト
    • クラス設計や適切な責務分担の判断が難しい場合もあります
  3. ディレクトリ構成の複雑化
    • Serviceオブジェクトパターンを増やしすぎるとファイルやクラスの管理が煩雑になることがあります

RailsにおけるServiceオブジェクトパターンの実装例

ここではユーザー登録処理を例にServiceオブジェクトパターンを実装してみましょう。

Step 1: ディレクトリ構成

通常app/servicesディレクトリを作成してServiceオブジェクトパターンを格納します。

app/
  services/
    user_registration_service.rb

Step 2: クラスの実装

以下はユーザー登録処理を行うUserRegistrationServiceの実装例です。

# app/services/user_registration_service.rb
class UserRegistrationService
  def initialize(user_params)
    @user_params = user_params
  end

  def call
    user = User.new(@user_params)

    if user.save
      send_welcome_email(user)
      { success: true, user: user }
    else
      { success: false, errors: user.errors.full_messages }
    end
  end

  private

  def send_welcome_email(user)
    UserMailer.welcome_email(user).deliver_later
  end
end

Step 3: コントローラでの使用例

コントローラではServiceオブジェクトパターンをシンプルに呼び出すだけです。

class UsersController < ApplicationController
  def create
    service = UserRegistrationService.new(user_params)
    result = service.call

    if result[:success]
      redirect_to user_path(result[:user]), notice: 'ユーザー登録が完了しました!'
    else
      flash[:alert] = "登録に失敗しました: #{result[:errors].join(', ')}"
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation)
  end
end

Serviceオブジェクトパターンを活用する際のベストプラクティス

  1. 単一責任を守る
    • 一つのServiceオブジェクトパターンは一つの目的に集中する
  2. 名前に明確さを持たせる
    • クラス名に役割を明示する例PaymentProcessingService
  3. 例外処理を組み込む
    • 予期しないエラーに備えて適切な例外処理を実装する
  4. テストを重視する
    • 独立したクラスであるためユニットテストを簡単に実装できる

おわりに

ServiceオブジェクトパターンはRailsアプリケーションの複雑なロジックを整理しコードの可読性と保守性を向上させる強力なツールです。 特にコントローラやモデルが肥大化しやすいプロジェクトではその効果を実感できるでしょう。