RailsのようなMVCアーキテクチャのフレームワークではControllerに処理を書くことを極力避けることでFat Controllerにならないようにするのはもう常識と言えるまでに浸透してますよね。
しかし最近はModelに何でもかんでも詰め込みすぎてFat Modelにならないようにしようという動きも有ります。
僕は最初、あぁそれでConcernに分けていくのねと思ったのですがConcernだけでかたづく問題ばかりではありません。
詳しくは
ここ 7 Patterns to Refactor Fat ActiveRecord Models
またはここあたり Object Oriented Rails – Writing better controllers
を読むのがいいようですが、そのなかにService Objectというのがあります。
今回そのService Obejctでモデルのデフォルト値をセットする処理を書いてみましたので紹介します。
まずTask管理をするRailsアプリがあったとしますTasksController#newでこんなコードになってました。
class TasksController def new @task = Task.new @task.category = category if params[:category_name].present? && (category = Category.find_by_name(params[:cateogry_name]).present? @task.due_date = params[:due_date].present? ? params[:due_date] : Date.tomorrow end end
ごちゃごちゃしていて分かりづらいですね。
いきなり結果ですが、前述のブログを参考に以下のように変えてみました
class TasksController before_action :load_task_default_value_service, only: [:new] def new @task = Task.new @task = @task_default_value_service.set(@task, params) end private def load_task_default_value_service(service = TaskDefaultValueService.new) @task_default_value_service ||= service end end
大分スッキリしました。
で肝心のService Obejctは app/services ディレクトリを作って配置します。このとき config/application.rb で config.autoload_path に app/services を追加しておかなければいけません。
app/services/task_default_value_service.rb
class TaskDefaultValueService def set(task, params) @params = params task.category = @cateogry if valid_category_name? task.due_date = due_date end def valid_category_name? @category = Category.find_by_name(params[:cateogry_name]) @params[:category_name].present? && @category.present? end def due_date @params[:due_date].present? ? @params[:due_date] : Date.today end end
たったこれだけの処理が専用のクラスに切り分けることでかなり読みやすくなりますね。
ここまでやって気になったんですがControllerからService Objectを呼び出しておく部分って冗長になりそうだなとおもったんです。
例えば今回だと before_action で load_task_default_value_service を実行するようにしてますが、これってService Objectの名称が変わるだけで同じ処理をたくさん書くはめになりそうです。
なので ApplicationController で method_missing を使ってService Objectの呼び出しを簡単に出来るようにしてみました。
class ApplicationController def method_missing(method_name, *arg) if method_name =~ /^load_([a-z_]+)_service$/ load_service_object($1) else super end end private def load_service_object(service_name) service_snakecase = (service_name + '_service') service = service_snakecase.camelize.constantize instance_variable_set("@#{ service_snakecase }", service.new) end end
こうしておけば UserAuthenticationService を使いたい時は
before_action :load_user_authentication_service
と書くだけでいいので簡単ですね!