👏

サービスクラスの骨組みとなる ServiceBase について

に公開2

こんにちは!

株式会社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? :渡されたパラメータをチェック(バリデーション)
    • 例えば、開始日付と終了日付の関係などをチェック。
  • resultexecuteで実行された処理の成否や、生成したデータなどを格納
    • 今回の場合は生成したcsvやエラーを格納。
    • また、ServiceBaseに定義したresult.success!failure!を呼ぶことで、このserviceexecuteの処理が正常に実行されたかを呼び出し元に伝える。

呼び出し側も統一されててスッキリ

先ほど定義した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 でも「ああ、この流れね」とすぐに把握できる。

  • 呼び出し方も共通化されている

    executeresult.success? の流れが共通しているので、ジョブやコントローラーからの利用時も迷わない。

  • 役割がはっきり分かれていて見通しが良い

    execute に実際の処理を書く、valid? にバリデーションを書く、result に結果をまとめる、という役割が分かれているので、

    「この処理どこに書こう?」と悩みにくく、読み手も流れを追いやすいです。

おわりに

この ServiceBase があることで、サービスクラスの作り方や呼び出し方が統一されていて、チームとしても開発しやすくなってるなーと感じます。

もちろん、チームやプロダクトによってベストな形は違うと思うんですが、「こういうスタイルがあるよ」という一例として参考になれば嬉しいです。

ここまで読んでくださってありがとうございました!

We are hiring!

TAIANでは、このような開発・技術・思想に向き合い、未来をつくる仲間を一人でも多く探しています。少しでも興味を持っていただいた方は弊社の紹介ページをご覧ください。

https://taian-inc.notion.site/Entrance-Book-for-Engineer-1829555d9582804cad9ff48ad6cc3605

TAIANテックブログ

Discussion