Railsにはポリモーフィック関連と呼ばれるものがある。
例えばこんな奴。
class Notification < ActiveRecord::Base
belongs_to :notifiable, polymorphic: true
end
class Message < ActiveRecord::Base
has_one :notification, as: :notifiable
end
class Like < ActiveRecord::Base
has_one :notification, as: :notifiable
end
一行で色んなクラスに対する関連が指定できて便利感がある。
だからって、これを安易に使う前にちゃんと考えよう。
ポリモーフィック関連は単に関連の定義を省力化するためのものじゃない。
ポリモーフィックという名前が示す様に、これは多態性を持ったものに対する関連を定義する事であって、インターフェースに対する関連の定義だということを理解して使うようにしよう。
Rubyは必要な時にメソッドさえ揃ってればちゃんと動くから簡単な内は余り意識しなくても動いてくれるのだが、ポリモーフィック関連として取得したものは、原則として同じ型のものとして扱えないといけないものだ。
境界の外からそのクラスを触る場合、それの詳細に立ち入らずにクラス自身の持っている情報に判断させるのが良い、という事だ。
これはポリモーフィック関連と並んで良く問題になるSTIでも同じこと。
Rubyでは必ずしもインターフェースの定義は必要じゃないが、ポリモーフィック関連にした時にはインターフェースが目に見えて分かるようにしておくのが良い。
開発者にちゃんとインターフェースを意識させるためにもそうしておく方が良いだろう。
具体的にはポリモーフィックの関連名でモジュールを作ってそこに振舞いのデフォルト実装を書き、各クラスでincludeする。
各クラスで必要ならそのメソッドをオーバーライドする。
上の例を元に適当に書くとこんな感じ。色んなものを省略してるので雰囲気だけ。
class Notification < ActiveRecord::Base
def deliver
if notifiable.notify_condition?
Notificator.new(notifiable).notify
end
end
end
module Notifiable
def title
raise NotImplementedError
end
def body
raise NotImplementedError
end
def notify_condition?
true
end
end
class Message < ActiveRecord::Base
include Notifiable
def notify_condition?
receiver.has_subscription?(:message)
end
end
class Like < ActiveRecord::Base
include Notifiable
def title
"あなたの記事が「いいね」されました"
end
def body
"#{liker.nickname}があなたの記事を「いいね」と言っています"
end
def notify_condition?
likeable.owner.has_subscription?(:like)
end
end
こんな感じで振舞いは各クラスが知っていて外からはそれを呼ぶだけ。処理の分岐はポリモーフィズムに任せる。
もし、ポリモーフィック関連を使っていて↓↓のようなコードがあったら、ほとんどの場合何かが間違っているという事を意識しておこう。
class Notification < ActiveRecord::Base
def deliver
case notifiable
when Message
if notifiable.receiver.has_subscription?(:message)
notifiable.send_message_notification
end
when Like
if notifiable.likeable.owner.has_subscription?(:like)
notifiable.send_like_notification
end
end
end
end
こうなってしまうと、読むのが辛い、テストが辛い、変更に弱い、と苦しみがガンガン増していく。
ポリモーフィック関連が個別のタイプの判別を必要とするような状況に陥らないように、使う前にインターフェースをしっかり考えよう。ドメインの中で同じ扱いをされているものなのかを考えよう。
ちなみに上の辛いcase文には更に上位バージョンが存在し、クラスの判定がtypeの文字列に行われている場合がある。
そうなると何がヤバイって、タイポしてても気付かずにelseに突入したり、サブクラスに対応できないなどの問題が更に増加する。
例を考えるのが割と面倒だったので、サンプルがなんかおかしくないか?みたいな突っ込みがあるかもしれないが、とにかくポリモーフィック関連の対象に対してtypeを尋ねるな、ということだけ何とか理解して欲しい。
コントローラーにそんなコードが混じってようものなら非常に辛いことになる。
ちなみにSQLアンチパターンにも書いてあるが、ポリモーフィック関連自体が、RDBに文字列でクラス名が入っていて安全ではない、外部キー制約が張れない等の理由で、割とデメリットも多いやり方だ。Railsの様にフレームワークのサポートが無い環境では更に慎重に考える必要がある。