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