例えば、「サービスクラスを実行するメソッドはクラスメソッドなのか、メソッド名は 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タスクで処理を行うためのクラスを実装する際にも制約を設けて役に立たたせることが可能です。