一定の制約を設けてRailsのサービスクラスに一定の秩序を設ける
Railsでのサービスクラス(またはオブジェクト)の是非はさておき、Railsでサービスクラスは導入するとルールがゆるふわになりがちだったりします。
例えば、「サービスクラスを実行するメソッドはクラスメソッドなのか、メソッド名は execute
なのか、perform
なのか、 call
なのか…
どれがいいとか悪いとかはないですが、これらに対して一定の制約を設けることで、サービスクラスに対して一定の秩序を与えていきましょう。
今回設定するサービスクラスへの制約について
- 必ずベースとなるmoduleをincludeすること。
- クラス名には必ず「Service」を含める。(含めないとエラーになるようにしています。)
initialize
は必ず定義して、インスタンス化させる。- 必ず、クラスメソッドから
perform
サービスクラスを実行する。
まず、サービスクラスのベースになるServiceBaseというモジュールを実装します。
module ServiceBase class ServiceClassRuleError < StandardError; end def self.included(base) base.extend(ClassMethods) raise ServiceClassRuleError.new("#{base.name}: Please Rename Service Class") unless base.name.include?("Service") end module ClassMethods def perform(*args) self.send(:new, *args).send(:perform) end end private def initialize raise NotImplementedError.new("You must implement #{self.class}##{__method__}") end def perform raise NotImplementedError.new("You must implement #{self.class}##{__method__}") end end
このベースになるモジュールの役割としては、
サービスのクラス名に
Service
という名称がクラス名に含まれているかをチェックしますinitalize
メソッドを必ず定義させるために、未実装の場合にエラーになるようにしています。perform
インスタンスメソッドを必ず定義させる。このインスタンスメソッドはperform
クラスメソッドからのみCallされるようにするために、気休め程度ですが、プライベートメソッドにしておきます。クラスを内部的に
new
して、インスタンス化して、perform
インスタンスメソッドをCallするための、perform
クラスメソッドを定義します。
というのが大きな役割です。
実際にServiceBase moduleを使ってサービスクラスを実装してみましょう。
class PostCreateService include ServiceBase private attr_reader :title, :content def initialize(title:, content:) @title = title @content = content end def perform Post.new(title: title, content: content).tap do |p| p.save end end end
次はControllerでサービスクラスをCallしてみましょう。(今回の例ではちょっとサービスクラスの旨味がないですが…)
# ... def create @post = PostCreateService.perform(title: post_params[:title], content: post_params[:content]) respond_to do |format| if @post.valid? format.html { redirect_to @post, notice: 'Post was successfully created.' } format.json { render :show, status: :created, location: @post } else format.html { render :new } format.json { render json: @post.errors, status: :unprocessable_entity } end end end # ...
暗黙的にクラスメソッドの perform
が実装されていて、意図しないクラス名が設定された場合でもエラーになるようになっています。
これでサービスクラスの実装する人も迷わなくて済みそうです。
応用例
今回、サービスクラスに対して一定の制約を設けるためにベースになるModuleを追加しましたが、 このModuleを応用してRailsRunnerだったり、Rakeタスクで処理を行うためのクラスを実装する際にも制約を設けて役に立たたせることが可能です。