初めに
この記事ではServiceクラスとは何かについてまとめます。
Serviceクラスとは
サービスクラスは、複数のモデルにまたがる処理や、特定のビジネスロジックをカプセル化するためのクラスです。
2つの視点からサービスクラスを見る
1. DDD(ドメイン駆動設計)の観点 ※DDDとは2003年にエリック・エヴァンスが提唱した設計アプローチのこと。
DDDにおけるサービスクラスは、エンティティや値オブジェクトに責務を持たせると不自然になる振る舞いを表現するものです。
重要なポイント:
- ユビキタス言語(業務用語)に由来した命名
- 「何が実行できるか」という振る舞いに着目
- エンティティの定義を歪めないための手段
例えば、銀行システムでの送金処理で見ると
❌
class Account < ApplicationRecord
def transfer_to(destination_account, amount)
# 送金元の残高チェック
# 送金元から引き落とし
# 送金先に入金
# トランザクション記録
end
end
Accountモデルに送金処理を書くと不自然。
この場合、「送金」という行為は 2つのアカウント間の関係性 を表すものであり、どちらか一方のAccountに責務を持たせるのは不自然になる。
✅
class MoneyTransferService
def initialize(source_account, destination_account)
@source_account = source_account
@destination_account = destination_account
end
def transfer(amount)
ActiveRecord::Base.transaction do
@source_account.withdraw(amount)
@destination_account.deposit(amount)
TransactionLog.create!(
source: @source_account,
destination: @destination_account,
amount: amount
)
end
end
end
2. パーフェクトRailsの観点 ※技術評論社が出したrailsに関する書籍
パーフェクトRailsでは、サービスクラスをコントローラーとモデルの中間に立つものとして位置づけています。
役割分担:
- コントローラー: HTTPリクエストをハンドリングするインターフェース
- サービス: モデルが行う処理を取りまとめるインターフェース
- モデル: データとそのデータに直接関連する振る舞い
重要なのは:
- 処理そのものをカプセル化する
- 業務知識と実装のメンタルモデルを一致させる
# app/services/user_registration_service.rb
class UserRegistrationService
def initialize(user_params)
@user_params = user_params
end
def register
ActiveRecord::Base.transaction do
user = User.create!(@user_params)
Profile.create!(user: user)
UserMailer.welcome_email(user).deliver_later
user
end
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error("User registration failed: #{e.message}")
nil
end
end
# コントローラー
class UsersController < ApplicationController
def create
service = UserRegistrationService.new(user_params)
@user = service.register
if @user
redirect_to @user, notice: '登録が完了しました'
else
render :new
end
end
end
サービスクラスを使うべき場合
少人数チームで開発している
- すぐにコミュニケーションが取れる
- コードレビューが密に行える
チームの技術水準が高い
- 対象業務を十分に理解している
- 読みやすいコードを維持することへの意識が高い
明確な基準がある
- チーム内でサービスクラスの使い方が統一されている
- どんな処理をサービスクラスに切り出すか明確
サービスクラスの問題点
基準が作りにくい
- 「一つのモデルに収まらない複雑な処理を表す振る舞いに名前をつける」
これは基準として曖昧すぎる。
問題として、
- どこまでをモデルに書き、どこからをサービスクラスに書くべきか不明確
- 開発者によって判断基準がバラバラになる
- 結果的にコードの一貫性が失われる
このようなことから、個人開発レベルなら問題ないが、業務で複数人で開発をしていくとなった時にサービスクラスは無闇に使うべきではない。
かといって、Fat Modelのままにするかといったらそういうわけにもいかない。
ここで折衷案として出るのがForm Model。
Formモデルとは
Formモデルとは、複雑なフォーム処理をモデルから切り離して管理するためのクラス
通常、Railsでは1つのフォームが1つのモデルに対応しますが、以下のような場合に困ります
複数のモデルを同時に作成・更新したい
例:ユーザー登録と同時にプロフィールも作成
データベースに保存しない一時的なフォーム
例:検索フォーム、お問い合わせフォーム
複雑なバリデーションが必要
例:利用規約への同意チェックなど
Formモデルの実装例
例:ユーザー登録フォーム(ユーザー + プロフィールを同時作成)
# テーブル構成は以下のようになっているとします。
# users テーブル: name, email, password, password_confirmation
# profiles テーブル: user_id, bio
# app/forms/user_registration_form.rb
class UserRegistrationForm
include ActiveModel::Model
include ActiveModel::Attributes
# フォームで受け取る属性
attribute :name, :string
attribute :email, :string
attribute :password, :string
attribute :password_confirmation, :string
attribute :bio, :string # プロフィールの自己紹介
# バリデーション
validates :name, presence: true
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, presence: true, length: { minimum: 8 }
validates :password_confirmation, presence: true
validates :bio, length: { maximum: 500 }
# 保存処理
def save
return false unless valid?
ActiveRecord::Base.transaction do
user = User.create!(
name: name,
email: email,
password: password,
password_confirmation: password_confirmation
)
Profile.create!(
user: user,
bio: bio
)
end
true
rescue ActiveRecord::RecordInvalid
false
end
end
このように2つのテーブルの内容をuser_registration_form.rbとしてユーザー登録フォームで入力する内容でまとめることができます。
しかし、ここまでFormモデルに関して説明しましたが、これはあくまで入力されたデータの妥当性に関するロジックというかなり限定的な部分のみ肩代わりできるというだけなので、Serviceモデルの代わりになるというわけではありません。
結論
難しい!
個人開発レベルならいいですが、チームで開発するとなると、このようなビジネスロジックはどこに書いたらいいとか、Serviceモデル使うべきかどうか、といったことを決めるのは難しいです。結局ServiceモデルもFormモデルも単なる技法に過ぎません。メンバー同士で、どういう場面で使って、なぜ必要なのかなどを話し合うしかないと思います。
具体的な解決策を期待していた方申し訳ございません。
終わりに
この記事の中で何か間違っているところなどあれば教えていただけると幸いです。
参考にしたサイト。もっと詳しいことが知りたい場合はこちらへ