308
267

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

そろそろポリモーフィック関連について一言いっとくか

Last updated at Posted at 2015-06-02

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の様にフレームワークのサポートが無い環境では更に慎重に考える必要がある。

308
267
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
308
267

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?