はじめに
Serviceクラスを使ったリファクタリングと、その後Serviceクラスを撤廃した経験をもとに、責務について考えたことをまとめます。
前提
- Rails API
- Ruby
- RSpec
なぜServiceクラスを使ったのか
「Fat Controller(ファットコントローラ)」解消のために導入しました。
もともと全てのロジックを含めて、コアとなる部分もコントローラーに書いていました。しかし、開発が進んでコード量が増えるにつれて、Fat Controllerになっていきました。
その後、責務を考え直し、Model側へリファクタリングしました。
Serviceクラスを使ってみて
Serviceクラスという名前だけでは責務が明確にならないため、何でもロジックを集める場所になってしまうことが問題だと感じました。
特に学習を始めた段階では、設計原則や基礎知識が十分に身についていないことから、Serviceクラスの責務が曖昧なまま使い続けてしまう懸念があります。
個人開発においてServiceクラスは使っていけないのか?
コード量が少ない段階であれば、Serviceクラスの責務も把握しやすいため、必要に応じて使ってもよいと思います。
しかし、開発が進んでコードが増えてきた場合、Service クラスの中身の把握が難しくなるかもしれません。
その際、Serviceクラスを使い続けるか、別の場所にリファクタリングするか改めて考える必要があると思います。
Serviceクラス以外の選択肢
いくつか方法があると考えており、選択肢に入れたものについてまとめます。
- Form Object
- concerns
- Model
※ コントローラーへ記述を戻すというのもありですが、振り出しに戻るため挙げません。よりコードを短くできるならいいかもしれませんが。
Form Object
Form Objectは入力値の受け取りやバリデーション、複数のModelにまたがるフォーム処理などを扱うための選択肢だと思いました。
今回扱うような、特定のModelに紐づくビジネスロジックを配置する目的には適さないと判断しました。
Concern
Concernは、共通のロジックや複数の場所で再利用できるもので、モジュールを通じて利用することができます。
今回のビジネスロジックは、複数の場所で共通して利用する処理ではなく、特定のModelに紐づく独自のロジックだったため、適さないと判断しました。
Model
今回の自分のケースでは、ビジネス上のルールが特定のModelに紐づいていたため、ServiceではなくModel側へ移すことにしました。
そもそもビジネスロジックとは?
ビジネスロジックについて、「システムのコア」や「システムの目的となる処理」と捉える考え方を学びました。
そして、データアクセス層(ファイルやデータへのアクセスのための読み書き)でもなく、プレゼンテーション層(アプリケーションとユーザーとのやり取り)でもないものだと理解しました。
私自身も「ビジネスロジック」という言葉を曖昧に使っていたため、今回のリファクタリングをきっかけに認識し直しました。
実際にServiceからModel側へリファクタリングしてみる
ここから、Serviceクラスに切り出していた処理を、app/models 配下のオブジェクトへ移した例を紹介します。
今回は、文字列を受け取って特定の文字列を数える処理を例にします。
文字列にhogeがどれくらい含まれているのかカウントするプログラム
Service クラス
class HogeCountService
def initialize(chars)
@chars = chars
end
def call
@chars.scan("hoge").length
end
end
コントローラー
class Api::V1::HogeController < ApplicationController
- 省略 -
def create
chars = params[:chars]
hoge_count = HogeCountService.new(chars).call
render json: hoge_count
end
- 省略 -
end
Model側にビジネスロジックをまとめる
今回はActiveRecordのModelそのものにメソッドを追加するのではなく、app/models配下に責務を持ったオブジェクトを配置しました。
Model側のテストが通ることを確認したら、不要になったServiceクラスとテストコードを削除します。
Model側にビジネスロジックを作成
module Hoge
class Count
def initialize(chars)
@chars = chars
end
def call
@chars.scan("hoge").length
end
end
end
コントローラー
class Api::V1::HogeController < ApplicationController
- 省略 -
def create
chars = params[:chars]
hoge_count = Hoge::Count.new(chars).call
render json: hoge_count, status: :created
end
- 省略 -
end
おまけ:テストコードの修正(RSpec)
spec/models/hoge/count_spec.rbで動作確認後、spec/services/hoge_count_service_spec.rbを削除します。
Service クラス
require 'rails_helper'
RSpec.describe HogeCountService do
describe '#call' do
let(:chars) { 'hogehogehogehogehoge' }
it 'returns 5' do
expect(described_class.new(chars).call).to eq(5)
end
end
end
Model
require 'rails_helper'
RSpec.describe Hoge::Count do
describe '#call' do
let(:chars) { 'hogehogehogehogehoge' }
it 'returns 5' do
expect(described_class.new(chars).call).to eq(5)
end
end
end
おわりに
Serviceクラスが絶対にダメというわけではありませんが、今回の自分のケースでは、Fat Controllerを解消するためにServiceオブジェクトへ処理を移したものの、結果的に責務が曖昧になっていました。
その過程で、名前空間の使い方やオブジェクト指向について理解を深めるきっかけになったのはよかったと思います。
こうした試行錯誤を通じて、良くないパターンに気づき、時には壊して、改善していく姿勢が大事だと思いました。
個人的には、責務が曖昧なServiceクラスを増やすことは避け、まずはそのロジックがどのオブジェクトの責務なのかを考えて、適切な場所にまとめていきたいと思います。
この記事が少しでも参考になりますと幸いです。
参考