【Rails】サービスクラスの責務と設計
はじめに
業務の中で「サービスで持つべき責務ってなんだっけ?」と考える機会がありました。本記事では、サービスが持つべき責務について振り返ります。サービスの責務を明確にすることは、ソフトウェア設計において重要なポイントです。責務を適切に分離することで、システムの可読性や保守性、拡張性を向上させることができます。本記事を通じて、サービスクラスの設計に関する理解を深めていければと思います。
サービスの主要な責務
一般的にサービスの責務は、主に以下の3つに分類できると言われます。
- ビジネスロジックの実装
- データ処理とトランザクション管理
-
外部システムとの連携
このセクションでは、これら3つの責務について具体的に説明します。
1. ビジネスロジックの実装
サービスの最も重要な責務は、ビジネスロジックを実装することです。ビジネスロジックとは、特定のビジネス要件やルールを実現するためのロジックです。サービスは、ビジネスルールを厳密に守り、正しいデータ処理を行うことが求められます。
class OrderService
def place_order(order)
unless validate_order(order)
raise ArgumentError, "無効な注文です"
end
# 注文を処理するロジック
save_order(order)
end
private
def validate_order(order)
# 注文の妥当性を検証するロジック
true
end
def save_order(order)
# 注文を保存するロジック
Order.create!(order)
end
end
2. データ処理とトランザクション管理
サービスはデータの処理とトランザクションの管理も担当します。データベースへの読み書きや、一連の操作がすべて成功することを保証するトランザクション管理を行います。
class PaymentService
def process_payment(payment)
ActiveRecord::Base.transaction do
# 支払い処理のロジック
Payment.create!(payment)
end
rescue => e
raise e
end
end
3. 外部システムとの連携
多くのサービスは、外部システムと連携する責務も持ちます。例えば、外部APIの呼び出しや、他のマイクロサービスとの通信などです。これにより、システムは他のサービスやアプリケーションと協力して動作します。
class NotificationService
def send_email(recipient, subject, body)
# 外部のメールサービスAPIを呼び出すロジック
ExternalEmailService.send_email(recipient, subject, body)
end
end
DDDにおけるサービスクラス
では、DDD(ドメイン駆動設計)においてサービスクラスはどのように位置づけられるのでしょうか?
サービスクラスの種類
DDDにおけるサービスクラスは主に以下の2種類に分類されます。
- ドメインサービス: ドメインのビジネスロジックを扱うサービスクラス。
- アプリケーションサービス: アプリケーションのユースケースを扱うサービスクラス。
ドメインサービスの責務
ドメインサービスは、エンティティやバリューオブジェクトに属さないドメインロジックを実装します。ドメインサービスが持つべき責務は以下の通りです。
- エンティティやバリューオブジェクトを操作するロジックの提供: 特定のエンティティやバリューオブジェクトに属さないロジックを実装します。
- ビジネスルールの実装: ドメイン内の重要なビジネスルールを実装します。
- 集約を跨る操作の実装: 複数の集約にまたがる操作を実装します。
https://www.kanzennirikaisita.com/posts/what-is-service-class
例
# app/services/domain/user_service.rb
class UserService
def initialize(user_repository)
@user_repository = user_repository
end
def change_user_email(user, new_email)
if valid_email?(new_email)
user.change_email(new_email)
@user_repository.save(user)
else
raise InvalidEmailError, "Invalid email format"
end
end
private
def valid_email?(email)
# メールアドレスのバリデーションロジック
email =~ URI::MailTo::EMAIL_REGEXP
end
end
アプリケーションサービスの責務
アプリケーションサービスは、ユーザーの入力を受け取り、アプリケーションのユースケースを実現するロジックを実装します。アプリケーションサービスが持つべき責務は以下の通りです。
- ユースケースの実現: アプリケーションのユースケースを実現するためのロジックを実装します。
- トランザクションの管理: 必要に応じてトランザクションを管理します。
- ドメインモデルの操作: ドメインサービスやリポジトリを呼び出してドメインモデルを操作します。
https://www.kanzennirikaisita.com/posts/what-is-service-class
例
# app/services/application/user_creation_service.rb
class UserCreationService
def initialize(user_params, user_repository, mailer)
@user_params = user_params
@user_repository = user_repository
@mailer = mailer
end
def call
user = User.new(@user_params)
if user.save
@user_repository.save(user)
@mailer.send_welcome_email(user)
user
else
nil
end
end
end
サービスクラスを使うメリット
上記のようなサービスクラスを使うことで得られるメリットは以下の通りです。
- 責務の明確化: エンティティやバリューオブジェクトに属さないビジネスロジックを分離し、責務を明確にできます。
- コードの再利用性向上: 共通のビジネスロジックをサービスクラスに集約することで、再利用性が向上します。
- テストの容易さ: ドメインサービスやアプリケーションサービスを単体テストしやすくなります。
これらのメリットを享受するためには、サービスクラスの設計において注意が必要です。
アンチパターン
一方で、サービスクラス設計時のアンチパターンについても触れておきます。
個人的には開発の途中でサービスクラスを導入する際は特に注意した方が良いイメージがあります。
1. 巨大なサービスクラス
サービスクラスが巨大化し、責務が多岐にわたると、メンテナンスが難しくなります。このようなサービスクラスは、しばしば「ゴッドクラス」とも呼ばれ、システム全体の凝集度を低下させます。
解決策
- シングル・レスポンシビリティ・プリンシプル(SRP)の適用: 各サービスクラスを一つの責務に集中させる。
- ドメインモデルの再評価: ビジネスロジックをエンティティや値オブジェクトに再分配する。
2. 無神経なサービス
サービスクラスがドメイン知識を持たず、単なるデータ操作の集まりになっている場合です。この場合、サービスクラスはドメインモデルと疎結合になり、ビジネスロジックが分散してしまいます。
解決策
- ドメイン知識の反映: サービスクラスがドメインのビジネスルールや知識を反映するようにする。
- エンティティと値オブジェクトの利用: ドメインロジックをエンティティや値オブジェクトに含め、サービスクラスを補助的な役割に留める。
3. すべてをサービスに
すべてのビジネスロジックをサービスクラスに押し込めることです。これにより、エンティティや値オブジェクトが単なるデータキャリアになってしまい、ドメインモデルの利点が失われます。
解決策
- バランスの取れた設計: ビジネスロジックを適切にエンティティ、値オブジェクト、およびサービスクラスに分散させる。
- リッチドメインモデルの作成: エンティティや値オブジェクトにビジネスロジックを持たせ、ドメインモデルを豊かにしておく。
サービスクラスの使い分けと設計方法
アンチパターンを避ける上でもサービスクラスで持つべき責務が何かを意識しながら設計しておくことは重要かもしれません。
ドメインサービスの設計と利用
ドメインサービスは、ドメインに関連するビジネスルールをエンティティやバリューオブジェクトから独立して実装するために使用されます。これにより、複数のエンティティにまたがるビジネスロジックを整理できます。
例
# app/services/domain/account_service.rb
class AccountService
def initialize(account_repository)
@account_repository = account_repository
end
def transfer_funds(source_account, destination_account, amount)
if source_account.balance >= amount
source_account.decrease_balance(amount)
destination_account.increase_balance(amount)
@account_repository.save(source_account)
@account_repository.save(destination_account)
else
raise InsufficientFundsError, "Source account does not have enough funds"
end
end
end
アプリケーションサービスの設計と利用
アプリケーションサービスは、ユーザーの入力を受け取り、ユースケースを実現するためのロジックを実装します。これにより、コントローラーのコードをシンプルに保つことができます。
例
# app/services/application/transfer_funds_service.rb
class TransferFundsService
def initialize(source_account_id, destination_account_id, amount, account_repository)
@source_account_id = source_account_id
@destination_account_id = destination_account_id
@amount = amount
@account_repository = account_repository
end
def call
source_account = @account_repository.find(@source_account_id)
destination_account = @account_repository.find(@destination_account_id)
AccountService.new(@account_repository).transfer_funds(source_account, destination_account, @amount)
end
end
コントローラーでの利用
コントローラーでは、アプリケーションサービスを利用してユースケースを実現します。
例
# app/controllers/accounts_controller.rb
class AccountsController < ApplicationController
def transfer
service = TransferFundsService.new(params[:source_account_id], params[:destination_account_id], params[:amount].to_f, AccountRepository.new)
begin
service.call
redirect_to accounts_path, notice: 'Funds transferred successfully.'
rescue InsufficientFundsError => e
redirect_to accounts_path, alert: e.message
end
end
end
まとめ
サービスが持つべき責務を明確にすることは、健全なソフトウェア設計の基本です。ビジネスロジックの実装、データ処理とトランザクション管理、外部システムとの連携といった責務を適切に分離することで、システムの可読性、保守性、拡張性を向上させることができるでしょう。今後のプロジェクトでも、この責務分離の原則を意識して設計を進めていきたいですね(自戒)
参考
Discussion