サービスクラスの骨組みとなる ServiceBase について
こんにちは!
株式会社TAIANでバックエンドエンジニアをしています、sekkey777です。
今回の記事では、TAIANのバックエンド開発において、Serviceの骨組みとなるServiceBaseについて紹介します。
アプリケーション開発において、ロジックが複雑な処理をモデルなどではなくServiceに切り出すことはよくあるかと思います。
TAIANでは、 Service に切り出す際、ServiceBaseという共通のベースクラスがあって、各クラスはそれを継承する形になっています。
私がジョインしたときには、既にこの形式で書かれており、最初は「ふーん」って感じでした。
今ではこれによってServiceの書き方が統一されていて良いなと思ったので簡単に紹介しようかと思います!
ちなみに、TAIANが開発しているアプリケーションのバックエンドには Ruby on Rails を採用しています。
そもそも ServiceBase って何してるの?
ざっくりいうと、「Serviceクラスを書くときの型」みたいなものです。
TAIANでは、ちょっと複雑な処理(csvやpdf生成とか、バッチ処理とか)は Serviceに切り出しています。
そのベースとして ServiceBase を継承する形で使っています。
中身はこんな感じ👇
class ServiceBase
attr_reader :result
class Result
attr_accessor :success
def success!
@success = true
self
end
def failure!
@success = false
self
end
def success?
success
end
def failure?
!success?
end
end
def initialize(result)
@result = result
end
def execute
raise NotImplementedError
end
def valid?
raise NotImplementedError
end
end
ざっくり説明すると
-
execute:実行処理を書くところ(各Serviceでオーバーライドして上書きます) -
valid?:バリデーション処理を書くところ(各Serviceでオーバーライドして上書きます) -
result:実行結果(成功 / 失敗、出力データ、エラーなど)を入れる
って感じの構成になってます。
実際に使ってるServiceBaseを継承したService
たとえば、あるデータをまとめたcsvを生成する処理は以下のような感じで書かれてます👇
class XxxCsvService < ServiceBase
attr_accessor :aaa, :bbb
class Result < ServiceBase::Result
attr_accessor :csv, :errors
def initialize
super()
@errors = []
end
end
def initialize(aaa, bbb)
super(Result.new)
@aaa = aaa
@bbb = bbb
end
def execute
return result.failure! unless valid?
result.csv = generate_csv(aaa, bbb)
result.success!
end
def valid?
result.errors << "aaaが不正です" if # aaaについてのバリデーションなど
result.errors << "bbbが不正です" if # bbbについてのバリデーションなど
result.errors.empty? # true か false を返すようにする
end
end
def generate_csv(aaa, bbb)
# 割愛します
# CSVのデータを集める処理
# CSVに入れ込む処理
# など
end
ここでやってることはシンプルです。
ServiceBaseに定義した雛形があるので、各メソッドをオーバーライドして実際の処理を書いています。
-
execute:実際に実行する処理を書く- 今回の場合はcsvを生成する処理を書く。
-
valid?:渡されたパラメータをチェック(バリデーション)- 例えば、開始日付と終了日付の関係などをチェック。
-
result:executeで実行された処理の成否や、生成したデータなどを格納- 今回の場合は生成したcsvやエラーを格納。
- また、
ServiceBaseに定義したresult.success!やfailure!を呼ぶことで、このserviceのexecuteの処理が正常に実行されたかを呼び出し元に伝える。
呼び出し側も統一されててスッキリ
先ほど定義したXxxCsvServiceは、ジョブやコントローラーから呼び出されますが、呼び出し方も統一されてて読みやすいです👇
class XxxCsvJob < ApplicationJob
def perform(aaa, bbb)
service = XxxCsvJob.new(aaa, bbb)
result = service.execute
raise StandardError, result.errors.join(',') if result.failure?
# メールに添付する場合は、その処理などを書く
end
end
TAIANでは基本的に、複雑な処理もこのパターンで書かれています。
そのため、他のServiceを読むときも、あらかじめ流れがわかっているので読みやすいと感じます。
個人的に良いと感じていること
-
書き方が統一されている
チーム全体で同じフォーマットで書かれているので、初めて見る
Serviceでも「ああ、この流れね」とすぐに把握できる。 -
呼び出し方も共通化されている
execute→result.success?の流れが共通しているので、ジョブやコントローラーからの利用時も迷わない。 -
役割がはっきり分かれていて見通しが良い
executeに実際の処理を書く、valid?にバリデーションを書く、resultに結果をまとめる、という役割が分かれているので、「この処理どこに書こう?」と悩みにくく、読み手も流れを追いやすいです。
おわりに
この ServiceBase があることで、サービスクラスの作り方や呼び出し方が統一されていて、チームとしても開発しやすくなってるなーと感じます。
もちろん、チームやプロダクトによってベストな形は違うと思うんですが、「こういうスタイルがあるよ」という一例として参考になれば嬉しいです。
ここまで読んでくださってありがとうございました!
We are hiring!
TAIANでは、このような開発・技術・思想に向き合い、未来をつくる仲間を一人でも多く探しています。少しでも興味を持っていただいた方は弊社の紹介ページをご覧ください。
Discussion
ご存知かもしれませんが、ActiveInteractionというgemは似たような思想でサービス層を導入できるようになっているのでおすすめです。
返信遅くなりすみません!
ありがとうございます!