カテゴリー:
Rails
タグ:
 Rails model mix-in

このエントリーをはてなブックマークに追加
更新日時:
2014年01月23日(木)
作成日時:
2014年01月23日(木)

前の記事 / 次の記事

継承すると訳分かんなくなるからmix-inでしょ常識的に考えて、
っていう話じゃなくて、そもそも訳が分からなくならなくても
継承してはいけない気がしたのでメモ。

STIしてると面倒だから親クラスに書いてしまいたくなるんだけど、
その場合でもやっぱりモジュールに切り出してmix-inした方がいいと思った。

以下理由。

class Item < ActiveRecord::Base
  belongs_to :world, counter_cache: true
end

class Weapon < Item
end

みたいなことをした時。

item = Item.last
item.world

weapon = Weapon.last
weapon.world

もできるけど、
キャッシュカウンターの挙動が期待を裏切る。

item = Item.new
item.save 
#=> world.items_count をインクリメント

weapon = Weapon.new
weapon.save
#=> world.items_count をインクリメント

キャッシュカウンターだけじゃなくて、
パラメーターの値が自身のクラス名を元に動的に決まるような場合、
親クラスのクラス名が使われるので期待したような動作にならない。

多分他にも動的に値が決まるヶ所は全部怪しい。

で、mix-inした場合は同じような問題は起こらないので
STIしてても面倒くさがらずにモジュールつくってmix-inした方が
いいに違いない、と思った。

module IsItem
  def self.included(base)
    base.class_eval {
      belongs_to :world, counter_cache: true
    }
  end
end

class Item < ActiveRecord::Base
  include IsItem
end

class Weapon < Item
  include IsItem
end

んだけど、mix-inしたらしたで微妙な問題があって、
コールバックを仕込んだ場合二重で発生する、ように見える。
ように見えるというのは、ように見えるだけで実際は二重には発生していない。

ていうかmix-inしなくてもそうなのかも知れない。

次のようなケース。
アイテムが保存される度に別のモデルに履歴を保存したいとする。

module IsItem
  def self.included(base)
    base.class_eval {
      after_create :create_log
      belongs_to :world, counter_cache: true
    }

    def create_log
      Log.create(name: name)
    end
  end
end

class Item < ActiveRecord::Base
  include IsItem
end

class Weapon < Item
  include IsItem
end

この時、Railsから見るとItem と Weapon の2つのモデルが
保存されるということになるのでコールバックが二回走る。

使ってるテーブルは同じだから当然コールバックは1回だけ走るとか
思い込んでると嵌る。