このエントリで書いた内容は、ほぼ Growing Rails Applications in Practice の内容が元になっています。英語ですが、ここで挙げた内容以外にもコードを綺麗に保つテクニックが書かれており、かつページ数も少なく読みやすいです。コードを綺麗に保つのが好きな方は一読してみることをおすすめします。
はじめに
Rails で fat model を避けるための方法は、7 Patterns to Refactor Fat ActiveRecord Models を始めとして、多くのやり方が存在します*1。
validation や callback は ActiveRecord(以下AR) を継承せずとも利用することができます。7 Patterns to Refactor Fat ActiveRecord Models の 「3. Extract Form Objects」がそのいい例ですね。ただ、(ARの)モデルに関する validation や callback をかけたい場合は、モデルに直接書いてしまう人が多いのではないでしょうか。
しかしそれは悲劇の始まりでもあります。例を挙げてみましょう。
例
友達と好きな趣味を共有しあうSNSのようなサービスを考えてみます。さらに、
- 自分自身の変更は友達に通知される
- 趣味は必ず1つ以上持っていなければならない
とします。すると User モデルは次のようになるでしょう
class User < ActiveRecord::Base has_many :friends, through: :friendships has_many :friendships has_many :hobbies, through: :likes has_many :likes validate :should_have_hobbies before_save :notify_friends private def notify_friends # 友達に自分自身の変更を通知するコード end def should_have_hobbies errors[:base] << '好きな趣味を登録してください。' if hobbies.count == 0 end end
「自分自身の変更は友達に通知される」を callback で、「趣味は必ず1つ以上持っていなければならない」を validation で実装しています。一見、何の問題もないように思えます。
それぞれ、具体的に何が辛いのか見ていきましょう。
callback の辛さ
このサービスは、毎日の運勢を占ってくれる機能があります。毎日バッチ処理でランダムに 0..100
の点数を出し、User の luck カラムに格納することにします。するとどうなるでしょうか。
ユーザは、友達全員の今日の運勢を通知として毎日受け取ることになってしまいますね。
もちろん、 skip_callback
メソッドを利用することで、 callback 処理をスキップさせることは可能です。しかし、通知をさせずに User を変更したいときに、毎回それを念頭に置いてコードを書くのは大変ですね。そしてそれを忘れてしまうともっと大変です。
また、before_save
などの callback 用メソッドに if や unless などのオプションを渡して、skip_callback
とは逆に、「callbackを実行したい時だけなんらかの処理をする」という仕組みにすることもできます。例えば
class User < ActiveRecord::Base before_save :notify_friends, if: :enable_notification attr_reader :enable_notification def use_notification @enable_notification = true end end
のようにして、
user.use_noticication user.save
のようにしたときだけ通知を飛ばすような仕組みです。これは一見うまいやり方に見えます。実際、アプリケーションコードが少ない時はこれでもうまく回ると思います。しかしアプリケーションコードが肥大してくると、大量の callback とその条件分岐のためのコードによりモデルが太ってきてしまいます。
さらに、上記の例だと条件が単純なため特に問題はないですが、複雑な条件を満たした時のみ callback を実行したいようなケースもありますね。その条件に対して、さらに callback 実行をオンオフするための条件式を追加するのはなるべくやりたくない感じです。
validation の辛さ
次に、User モデルのユニットテストを書いてみましょう。User を save したときに、ちゃんと User#notify_friends
が実行されるかをテストしてみます。素直に書くとこんな感じでしょうか。
require 'rails_helper' RSpec.describe User, type: :model do describe '#notify_friends' do it 'save したときに、User#notify_friends が実行されていること' do user = User.new expect(user).to receive(:notify_friends) user.save end end end
しかしこれだとうまくテストが通りません。なぜでしょうか。
validate :should_have_hobbies
があるので、関連する hobbies がないと validation が通らず、 callback が実行されないからですね。次のように Hobby を作り、User と関連付けさせれば通ります。
hobby = Hobby.create!(name: 'ジョギング') user = User.new user.likes.create!(hobby: hobby) user.save
しかし、User を保存するテストコード全てに、このようなコードを追加していくのは大変です。factory_girl などの fixture replacement を使うことで大変さをいくらか減らせますが、そうすると今度は factory_girl の定義自体の複雑さが増して辛いことになったりします。
ではどうするとよい?
ここまで、 callback や validation をモデルにそのまま書くことにより、それらを必要としない場合に複雑さをもたらす例を見てきました。この複雑さを解消するにはどのようにするべきでしょうか?
それには継承を使います。具体的なコード例として、先述の User モデルを書きなおしてみます。
class User < ActiveRecord::Base has_many :friends, through: :friendships has_many :friendships has_many :hobbies, through: :likes has_many :likes end
class User::AttributeUpdator < User before_save :notify_friends validate :should_have_hobbies private def notify_friends # 友達に自分自身の変更を通知するコード end def should_have_hobbies errors[:base] << '好きな趣味を登録してください。' if hobbies.count == 0 end end
通常の User と、何らかの属性を変更するとき用に使う User::AttributeUpdator というクラスに分け、User::AttributeUpdator は User を継承するようにしました。User::AttributeUpdator は User のサブクラスなので、当然 User 側で定義している has_many の関連も使えます。
これにより、他の友だちに通知させたいときは User::AttributeUpdator を利用し、そうでないときは User を使うという形で使い分けをすることができます。
必要なときに必要な分だけの validation や callback を設定することができ、さらにそれぞれの処理をサブクラスに移すことで User モデルがスリムになりました。
やりましたね!…と言いたいところですが、いくつか注意事項があります。
継承を利用する場合の注意点
form_for
や url_for
に ActiveRecord のオブジェクトを引数として渡した時、URLの組み立てに model_name
が使われます。そして User::AttributeUpdator.new.model_name.to_s #=> 'User::AttributeUpdator'
なので、User::AttributeUpdator
オブジェクトを渡した時は意図した URL が作られないことになります。
また、User
モデルが STI を利用していたとしましょう。その場合 User::AttributeUpdator
は STI とは違うので、 type カラムに入れたくないですね。
これらを解決するために、active_type という gem を使います。
普通に Gemfile に入れて bundle install
して、User::AttributeUpdator
の定義箇所を次のようにするとうまく動きます。
class User::AttributeUpdator < ActiveType::Record[User] #... end
ちょっと見慣れない書き方ですね。やっていることはそれほど複雑ではないので、気になる人は独自の gem を作ってしまってもいいかもしれません*2。
クラスの変更
ログインが必要なアプリーションを作る時、大抵次のように current_user
のようなメソッドを定義することになると思います。
def current_user @current_user ||= User.find(session[:user_id]) end
ログインユーザが自分の情報を変更するケースを考えてみましょう。この場合は User::AttributeUpdator を利用するのでしたね。しかし current_user は User クラスのオブジェクトを返してきます。困りました。
そこで、User オブジェクトを User::AttributeUpdator オブジェクトに変換します。
先ほど紹介した active_type が変換用のメソッドを用意してくれています。
user = ActiveType.cast(current_user, User::AttributeUpdator) user.update(params[:user])
おわりに
継承を使うことにより、callback や validation を分割し、必要なときのみ使えるようにすることができました。このテクニックを実際に仕事のコードとして使ってみましたが、想定通りに整理されたコードを作ることが出来てなかなかよい感じです。
しかし、このテクニック自体はあまり認知されておらず、知らない人が見ると面食らう可能性があるのでもうちょっと普及して欲しいところです。
また、ここで紹介したよりももっと良い方法があるのではないか?という気持ちもあります。もし、もっといい方法知ってるぜ!という方がいたらブログのコメントなりTwitterでメンションなりいただけると嬉しいです (\( ⁰⊖⁰)/)
*1:パーフェクト Ruby on Rails でも一部触れていますね
*2:作ろうと思ったのですがなかなか時間取れないのでした