Ruby on Railsでサービスを作ってみた おまけ
実装周りで補足した方が良いことを思いついてしまったのでおまけとしてメモ。いつもの様に過去記事はこちらからどうぞ。(part1、part2、part3、part4、part5、part6)
active_decorator
今回active_decoratorを使ってFat Modelになるのを防いでいる。active_decoratorはビューで使うメソッドを別ファイルで管理することが出来るようにしてくれる。使い方も簡単でrakeで対象のモデルをジェネレートしてそこにロジックを記載していくだけ。
./bin/rails g decorator question
こうすると以下のmoduleが作成される。
module QuestionDecorator end
この中にガシガシメソッドを書いていくわけだが、コンテキストとしてはモデルなのでモデルのattributesやメソッドにはアクセスすることが出来る。特徴として、モデル側に何の処理も書かなくても勝手に追加される。その代わり命名ルールに従う必要がある。そして、ビューの時しかデコレートされない。これはコントローラからビューに渡す時にフックしてデコレートしているからだ。コントローラから使うメソッドは元のモデルに記述する必要がある。
そして親子関係のモデルの場合は、自動的に子供のモデルはデコレートされず、renderメソッドに渡した時にデコレートされる。つまりrender partialで使うのが自然な流れというわけだ。
これはビューが大きくなるのを防ぐ意味がある。親をレンダリングするテンプレートと子供レンダリングするテンプレートを分けることになるからだ。子供のテンプレートが再利用出来ればなお良しというところだろう。
STIとの組み合わせ
ここまでは普通のactive_decoratorを使っているだけなのだが、今回Single table inheritance(STI)と組み合わせて使ってみた。STIは単一継承テーブルで、DBのテーブル的には1つだが、特定のカラムの値によってモデルクラスを変える仕組みだ。何が嬉しいのかというと、モデルの中のメソッドで自分がどういう情報かによって分岐を入れることがある。今回のsurveyの場合で言うと質問がそれにあたる。回答が単一選択なのか複数かのか、フリーフォーマットなのか日付選択なのかが別れるわけだ。質問モデル1つでだけであると、メソッドの中で分岐を入れる必要が出てきて、メソッドが複雑化してしまう。最初からクラス分けをしておけば、分岐はしなくてよくなるということである。
今回の例で言うと以下の様な継承関係になる。それぞれ特有の処理はそれぞれのクラスの中に書けばいいわけだ。共通のメソッドは親クラスに記述する。
class Question < ActiveRecord::Base end # 単一選択 class SingleChoice < Question end # 複数選択 class MultipleChoice < Question end # 日付選択 class DateChoice < Question end # フリーフォーマット class FreeChoice < Question end
デフォルトではtypeというstringカラムを追加すればSTIとして認識される。そこに入る値としてはクラス名が入る。子クラスで検索をかければ同じクラスだけが取得出来、親クラスで検索すれば全パターンの種類が取れる。
# 親クラスで検索 Question.all => #<ActiveRecord::Relation [#<MultipleChoice id: 42, surveys_id: 1, no: 1, text: "好きなスポーツ", type: "MultipleChoice", value: "[{\"value\":\"1\",\"text\":\"野球\"},{\"value\":\"2\",\"text\":\"サッ...", image: nil, created_at: "2015-09-24 19:10:02", updated_at: "2015-09-30 08:51:01">, #<SingleChoice id: 43, surveys_id: 1, no: 2, text: "好きなフルーツ", type: "SingleChoice", value: "[{\"value\":\"1\",\"text\":\"りんご\"},{\"value\":\"2\",\"text\":\"み...", image: "88d78f8d-b99e-417c-b976-c7fb3784fb93.jpg", created_at: "2015-09-24 19:10:02", updated_at: "2015-09-30 08:51:1:13">, ...]> # 子クラスで検索 SingleChoice.all => #<ActiveRecord::Relation [#<SingleChoice id: 43, surveys_id: 1, no: 2, text: "好きなフルーツ", type: "SingleChoice", value: "[{\"value\":\"1\",\"text\":\"りんご\"},{\"value\":\"2\",\"text\":\"み...", image: "88d78f8d-b99e-417c-b976-c7fb3784fb93.jpg", created_at: "2015-09-24 19:10:02", updated_at: "2015-09-30 08:51:01">, #<SingleChoice id: 60, surveys_id: 16, no: 1, text: "好きなドリンクは?", type: "SingleChoice", value: "[{\"value\":\"1\",\"text\":\"コーラ\"},{\"value\":\"2\",\"text\":\"オ...", image: nil, created_at: "2015-09-25 14:47:48", updated_at: "2015-09-25 14:47:48">, #<SingleChoice id: 61, surveys_id: 15, no: 1, text: "ほげもげ", type: "SingleChoice", value: "[{\"value\":\"1\",\"text\":\"hoge\"},{\"value\":\"2\",\"text\":\"...", image: nil, created_at: "2015-09-26 04:52:16", updated_at: "2015-09-26 04:52:16">, ...]>
さて、どちらもFat Modelを回避するために使用出来るのだが、アプローチや考え方が違う。STIでのクラスは別クラスと言ってもモデルクラス(たまたま同じテーブルを指している)なので、親クラスのデコレートmoduleに書いたことは使えない。つまり
module QuestionDecorator def hello 'hello' end end # questionがSingleChoiceクラスのインスタンスだとする # slimテンプレートで呼び出せない = question.hello
なのでデコレータはSTI毎に作ることで回避した。だが、共通の処理とかもあるのでそれは親のデコレータに記述し、子デコレータが親をincludeする形で対応した。
module QuestionDecorator # 共通のメソッドを記述 end # 単一選択デコレータ module SingleChoiceDecorator include QuestionDecorator end # 複数選択デコレータ module MultipleChoiceDecorator include QuestionDecorator end # 日付選択デコレータ module DateChoiceDecorator include QuestionDecorator end # フリーフォーマットデコレータ module FreeChoiceDecorator include QuestionDecorator end
ここまで分ける必要があるのかというのはあるが、こうすることでFat Model化はだいぶ防ぐことが出来る。(逆に理解していないとどこに書けばいいのかわからなくなりそうだが・・・)
enumについて
STIを使わない場合はactiverecordのenumをtypeに定義することで便利なメソッドが使えていた。良くあるのが判定と更新処理だ。
if question.single? # 単一選択の場合の処理 end # 単一選択にtypeを設定して更新する question.single!
このメリットを失いたくなかったのでenum定義自体は残すことにした。
enum type: { single: 'SingleChoice', multiple: 'MultipleChoice', date: 'DateChoice', free: 'FreeChoice', }
こうすることでenumでできていた事を失わずにSTIのメリットも享受出来る。ただ、クラス名が重複してしまいDRYの原則からは外れている。STI側で上手いこと対応してくれないものだろうか。
まとめ
組み合わせた場合に記述する方針としては以下のようになる。
場所 | 記述内容 |
---|---|
STI 親クラス | コントローラ、ビューで使う共通処理 |
STI 子クラス | コントローラ、ビューで使う個別処理 |
親クラス デコレータ | ビューだけで使う共通処理 |
子クラス デコレータ | ビューだけで使う個別処理 |
STIはオブジェクト指向としては非常にマッチした設計の一つだ。今回はそれを使いつつ、ビューの処理を分けるactive_decoratorと組み合わせて使うことでFat Model退治を行うことが出来た。今回のように両方使わなくても、どちらか片方だけでも使うだけでも十分効果を発揮するだろう。