chira-ura.html

[Rails] コンパクトにサービス層を自作・導入する(ジェネレータ付き)

Word count: 779 / Reading time: 4 min
2018/06/10 Share

TL;DR

  • Railsアプリにサービス層を導入したいんだけど、Trailblazerとか使うほどでもないんだよなぁ、という人(主に自分)向け
  • 素のRailsアプリに自作サービス層を導入してみた
  • ついでにrails generate service hogeみたいなジェネレータも作成してみた

経緯

既存のRailsアプリ(APIサーバ)にサービス層を導入しよう!と思い立ったものの、
Trailblazer等のgemを導入すると大掛かりな改修が必要になるため、少々敷居が高いと感じました。
そこで、既存プロジェクトにgemを追加せず、最低限の機能を持ったコンパクトなサービス層を自作して導入しようと考えました。

要件(サービス層に求めていたこと)

  • 既存のRailsプロジェクトにgemを追加せずに導入できること
  • rails generate service sampleのように、ジェネレータを用いてファイルを作成できること
  • 1機能 1クラスのコンパクトなサービスクラス
  • パラメータのバリデーションを行い、異常があれば例外を投げる(既存コードと同様のエラー処理を行うため)

方針

  • ActiveModel::Modelをincludeしたクラス(フォームオブジェクトのようなもの)を作成する
  • 処理実行前のバリデーション実行を強制し、パラメータが正常な場合のみ処理を行うようにする
  • バリデーションエラー時にはActiveRecord::RecordInvalid例外を投げる
  • rails generate generatorを使用し、ジェネレータを生成する
  • パラメータはコンストラクタ経由で渡す(個人的な好みです)

ソースコード

app/services/base_service.rb

すべてのサービスクラスの親クラスです。
ポイントは、

  • ActiveModel::Modelをincludeし、パラメータのバリデーション機能を使用可能にする
  • 処理実行前に必ずバリデーションを経由させる

の2点です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class BaseService
include ActiveModel::Model

# サービスを利用するクラスが呼び出すメソッド
def provide()
raise_validation_error if invalid?
perform
end

private

# サービスの処理(パラメータが正常な場合のみ実行されます)
# 各サービスクラスではこのメソッドをoverrideして機能を実装します
def perform()
raise NotImplementedError.new("You must implement #{self.class}##{__method__}")
end

# バリデーションエラー時、ActiveRecord::RecordInvalidを投げるためのメソッド
def raise_validation_error()
raise ActiveRecord::RecordInvalid.new(self)
end
end

app/services/sample_service.rb

個々のサービスクラスは、上記のBaseServiceクラスを継承して作成します。
パラメータはコンストラクタ内でインスタンス変数にセットし、処理本体はperform()メソッド内に実装します。
また、必要に応じてバリデーションのためのコードを記述します。

(コードの雛形は後述するジェネレータで自動生成します)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SampleService < BaseService
# パラメータ一覧
attr_accessor :username, :password

# バリデーション
validates :username, presence: true
validates :password, presence: true

# パラメータはコンストラクタで渡す
def initialize(username, password)
@username = username
@password = password
end

private

# 処理本体(パラメータが正常な場合のみ実行されます)
def perform()
# ここに処理を記述します
end
end

ジェネレータ

ジェネレータは下記のコマンドで追加できます。

1
$ rails generate generator service

コマンド実行後、lib/generators/serviceが作成されるので、ジェネレータのコードを編集します。
ジェネレータクラスにあるすべてのインスタンスメソッドが(書いた順番に)実行されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ServiceGenerator < Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)

# パラメータを定義
argument :name

# サービスクラスを作成する
def create_service_file
destination = Rails.root.join("app/services/#{name.underscore}_service.rb")
template('service.rb.erb', destination)
end

# サービスクラスのテストクラスを作成する
def create_test_file
destination = Rails.root.join("test/services/#{name.underscore}_service_test.rb")
template('test.rb.erb', destination)
end
end

また、サービスクラスとそのテストクラスのテンプレートは下記の通りです。

サービスクラスのテンプレート

1
2
3
4
5
6
7
8
9
10
11
class <%= name.camelize %>Service < BaseService
def initialize()
# Set parameters...
end

private

def perform()
# Run this method if parameters are valid
end
end

テストクラスのテンプレート

1
2
3
4
5
6
7
require 'test_helper'

class <%= name.camelize %>ServiceTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

使い方

サービスクラス作成

下記のコマンドを実行します

1
$ rails generate service sample

実行後、app/services/sample_service.rbtest/services/sample_service_test.rbが作成されますので、
適宜修正してください。

controllerからの呼び出し

前述のように、パラメータはコンストラクタの引数として渡します。
また、provide()メソッドを呼び出すことで、処理実行前に自動でバリデーションが行われます。

1
2
service = SampleService.new('myuser', 'mypassword')
result = service.provide

まとめ

ActiveModel::Modelを利用し、コンパクトなサービス層の導入を実現しました。
また、ジェネレータも用意し、簡単にサービスクラスを追加できるようにしました。

今回作成したコードはかなり単純なものなので、もう少し改良してからgemにできたらと思います。

参考

trailblazer/trailblazer: A High-Level Architecture for Ruby.
Rails:Service層を運用して良かったところ、悪かったところ - Qiita
Rails のアーキテクチャ設計を考える - Qiita
rails でカスタム generator 作る話 - scramble cadenza
erikhuda/thor: Thor is a toolkit for building powerful command-line interfaces.

Author: Kohei Kakimoto

URL: https://sashimi343.github.io/chira-ura/2018/06/10/rails-service-layer/

Published at: 2018-06-10(Sun) 16:43:55

License: Copyright (c) 2018 Kohei Kakimoto

CATALOG
  1. 1. TL;DR
  2. 2. 経緯
  3. 3. 要件(サービス層に求めていたこと)
  4. 4. 方針
  5. 5. ソースコード
    1. 5.1. app/services/base_service.rb
    2. 5.2. app/services/sample_service.rb
    3. 5.3. ジェネレータ
      1. 5.3.1. サービスクラスのテンプレート
      2. 5.3.2. テストクラスのテンプレート
  6. 6. 使い方
    1. 6.1. サービスクラス作成
    2. 6.2. controllerからの呼び出し
  7. 7. まとめ
  8. 8. 参考