2020-05-20

Polymorphicについて

STIやったなら、ポリモーフィック(ポリモーフィズム)についてもやらんといかんだろう
ということでポリモーフィックについても調べてみた
知ってることでもちゃんと言語化するのは大切だとSTI書いた時に学んだ

ポリモーフィックとは

  • GoFのデザインパターンで言うところの「プログラムは実装に対してではなく、インターフェースに対して行う(Program to an 'interface', not an 'implementation'.)」を実現する方法の一つ
  • インターフェースとは「ある決まった振る舞い」「入出力の定義」のこと
  • ポリモーフィックについて一言で表現すると「類似したクラスに対するメッセージの送り方を共通にする仕組み」
    • 「具体的にどのクラスのインスタンスであるかを気にせずメッセージを送れる仕組み」と言ってもよい
  • 要するにメッセージを送る側が楽をする仕組み

プログラムで見てみよう

↑の説明でわかるならこんな記事を読む必要がないので、どういうことなのかを素のRubyで書いてみる

class Animal
  def cry
    raise NotImplementedError
  end
end

class Baby < Animal
  def cry
    puts "オギャー"
  end
end

class Dog < Animal
  def cry
    puts "ワン"
  end
end

# 相手が誰であっても鳴けと指示するトレーナークラスを定義する
# executeメソッドの引数はAnimalクラスまたはサブクラスのインスタンス(であることを想定している)
class Trainer
  def self.execute(animal)
    puts animal.cry
  end
end

これが基本

ではRailsでのポリモーフィックは

Railsのポリモーフィック関連は、テーブルに 〇〇_id〇〇_type カラムを作成し、モデルで関連を貼るだけで使用することができる
↑のサンプルではポリモーフィックにし辛いので、ユーザーまたは企業が記事を投稿できるようなアプリを考えてみる

まずは記事のテーブルの作成
(usersとかcompaniesは省略)

# 記事なんだからbodyとかないとアレだが無視
create_table "posts", force: :cascade do |t|
  t.string "title"
  t.string "postable_type"
  t.bigint "postable_id"
  t.index ["postable_type", "postable_id"], name: "index_posts_on_postable_type_and_postable_id"
end

続いてモデルの作成

class Post < ApplicationRecord
  belongs_to :postable, polymorphic: true
end

class User < ApplicationRecord
  has_many :posts, as: :postable
end

class Company < ApplicationRecord
  has_many :posts, as: :postable
end

これで posts テーブルには以下のようなレコードが保存されていく

id postable_id postable_type title
1 1 User 記事名1
2 1 Company 記事名2
3 2 User 記事名3

そして、記事からは post.postableUserCompany のインスタンスが取得できる
ポリモーフィックなので、それぞれに共通のメソッドを定義してあげないといけない

class User < ApplicationRecord
  has_many :posts, as: :postable

  def author_name
    # last_nameとfirst_nameはusersテーブルのカラム
    "#{last_name} #{first_name}"
  end
end

class Company < ApplicationRecord
  has_many :posts, as: :postable

  def author_name
    # nameはcompaniesテーブルのカラム
    name
  end
end

post = Post.first
# postableがなんなのか気にする必要ない!
post.postable.author_name

Post が最初のサンプルにおける Trainer にあたる
post.postable.XXXX みたいなコードを書くのであれば、 postable でアクセスされる可能性のあるモデルでは必ず XXXX に当たるメソッドを提供しなければならないのがキモ
そして共通のインターフェース(ここでは author_name)さえ提供されていれば、その中はどんな処理でも構わない(ただし返り値は統一されている必要がある)

もしポリモーフィック関連で case 文とか使ってモデルによって分岐とかしていたら、何かがおかしいことになっている可能性が高いというか間違いなくおかしなことになっている

ポリモーフィック関連を使うのであれば

  • ポリモーフィック関連の対象にはtypeを聞いてはいけない
  • 〇〇_type カラムにクラス名が入るので、STIと同じデメリットがDBに発生する

と言う点については最低限覚えておく必要がある

参考