Help us understand the problem. What is going on with this article?

Railsのポリモーフィック関連とはなんなのか

More than 1 year has passed since last update.

はじめに

Railsのポリモーフィック(polymorphic、多態性)関連について、実装方法を説明している記事は見かけるのですが、実際これがどんなものどんな時に便利なのかを説明している記事があまりなく、よくわからないで使っている人もいるようなので本記事を書きました。

実装方法そのものについては詳細には説明しませんので、Railsのポリモーフィック関連をどう書くのかを知らない方はまずは公式ドキュメントを読んでいただけると理解が早いかと思います。
ボリュームは少ないので、2分もかからないかと思います。
Rails Guides: 2.9 ポリモーフィック関連付け

"どんなものか"について、先に結論

ポリモーフィックとはダックタイピングの一種であり、
別の言い方をするとGoFのデザインパターンで言うところの「プログラムは実装に対してではなく、インターフェースに対して行う(Program to an 'interface', not an 'implementation'.)」を実現する方法の一つになります。

この方針はポリモーフィックだけで無くSTI(Single Table Inheritance: 単一テーブル継承)などでも同じです。

......と言われて何のことかわかる人はこの記事を読む必要はないかもしれません。
わからない人は安心してください。これから説明していきます!

ダックタイピング/インターフェースとは

ポリモーフィックの話をする前に、まずはダックタイピング/インターフェースについて解説していきます。

まずは、以下のサンプルコードをご覧ください。

class Animal
  def initialize(animal)
    @animal = animal
  end

  def bark
    @animal.sound
  end
end

# 「この場合だとAnimalを継承する方が正しいのでは?」とかツッコミどころはあるかとは思いますが、
# そこはスルーでお願いします......
class Duck
  def sound
    'quack'
  end
end

class Cat
  def sound
    'myaa'
  end
end

# 使い方
Animal.new(Duck.new).bark #=> "quack"
Animal.new(Cat.new).bark  #=> "myaa"

ダックタイピングの語源は以下の言葉です。

"If it walks like a duck and quacks like a duck, it must be a duck"
「もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルである」

これを上記サンプルコードに則して表現すると以下のように言えるでしょう。

「もしもそれが動物(Animal)のように鳴く(#soundメソッドを持っている)ならば、それは動物である。」

このように、「違うものがある決まった振る舞い/入出力を持つことで、同じように扱えるようにすること」をダックタイピングと呼びます。
そして、その「ある決まった振る舞い」「入出力の定義」のことをインターフェースと呼びます。

コード例に沿って表現すると、DuckCatは違うクラスですが両方とも#soundというメソッド(インターフェース)を持つので、Animalの中ではどちらのオブジェクトであるかを意識せず#soundを呼ぶことができる、というのがダックタイピングです。

ダックタイピングのメリット

さて、では上記のコードは何が便利なのでしょうか?
ここで注目して欲しいのは、上記コードは以下のようになっていることです。

  • Animalクラスの@animalに入るオブジェクトは、#soundを呼べるオブジェクトである
  • #soundが存在すること」、「#soundの返り値の形式が同じこと」が守られていれば、引数となるクラスは#barkメソッドの内部実装を知る必要がないし、手を加える必要はない

これらを守らないNG例を書いてみました。

# NG例

class Animal
  def initialize(animal)
    @animal = animal
  end

  def bark
    # @animalの実態ごとにメソッドの呼び方を変えなければならない。
    # そのため、条件分岐が@animalに入りうるクラスの数だけ増える。
    case @animal
    when Duck
      @animal.sound
    when Cat
      # @animal側の実装を理解して、返り値をちゃんと受け取るようにしなければいけない
      @animal.cry[:sound]
    end
  end

  def run
    # メソッドの数だけ条件分岐を書かなければならないので、複雑かつ冗長になる
    case @animal
    when Duck
      @animal.walk
    when Cat
      @animal.run
    end
  end
end

class Duck
  def sound
    'quack'
  end

  def walk
    'walk'
  end
end

class Cat
  # Duck#soundとインターフェース(メソッド名、返り値)が違う
  def cry
    # 返り値の形式がDuckと違う
    { sound: 'myaa' }
  end

  def run
    'run'
  end
end

コード内にコメントで書いたように、色々問題が出てきます。

先ほどのルールが守られていれば、例えば新しいクラスを追加しなければならない場合にも「既存コードはいじらず」「新しく追加するコード側のインターフェースのみ気を付ければ良い」ようになるので、複雑度を上げないで済みます。

# 例えばこんなクラスを追加する
class Dog
  # インターフェースを踏襲(ダックタイピング)することで、既存コードはいじらずに機能拡張を行える。
  # インターフェースが違う場合はAnimal側も修正が必要になる。
  def sound
    'bow-wow'
  end
end

# `Duck`, `Cat`と同じように扱える
Animal.new(Dog.new).bark #=> "bow-wow"

# 既存コードはいじっていないので、当然これまでの実装が壊れることはない
Animal.new(Duck.new).bark #=> "quack"
Animal.new(Cat.new).bark  #=> "myaa"

このように、インタフェースを統一することで冗長なコードがなくなり、スケーラビリティを担保できることがダックタイピングのメリットです。

ポリモーフィックの説明、その前に

さて、ようやくポリモーフィックの説明です。

まず、これもサンプルコードがあった方が解説しやすいので、以下のようなアプリがあったとして説明をさせていただきます。

サンプルアプリ要件

  • あるモデル間でメッセージを送り合える
    • LINEとかそういうものだと考えてください
  • (とりあえず)メッセージ送信者となるモデルにはEmployer, Employeeの2つがある
    • 本来ならばこれらをまとめるUserモデルがいるべきかもしれませんが、今回のアプリでなかったとします

メッセージモデル(テーブル)の定義

さて、上記のようなメッセージアプリを作る時、メッセージのモデル(テーブル)はどのように作ればいいでしょうか?

もしポリモーフィックを使わずにやるならば、以下のようにEmployerMessageモデル(employer_messagesテーブル)とEmployeeMessageモデル(employee_messagesテーブル)を作り、thread_idのような両テーブルに共通するカラムを用意してJOINする、などという方法が考えられます。

class Employer < ApplicationRecord
  has_many :employer_messages
end
class EmployerMessage < ApplicationRecord
  belongs_to :employer
end

class Employee < ApplicationRecord
  has_many :employee_messages
end
class EmployeeMessage < ApplicationRecord
  belongs_to :employee
end

要件によってはこれもありなのかもしれません

ただ、EmployerMessageEmployeeMessageが同じインターフェースを持つのならば、この方法では同じコードを繰り返し書くことになるのであまり好ましくありません。
また、メッセージ送信者モデルが増えれば増えるほどコードの重複も増え、複雑度もそれに応じて高くなることが予想されます。

ですので、メッセージモデルは一つにまとめ、それを各メッセージ送信者モデルで共有したくなります。

その場合でポリモーフィックを使わないならば、コードは以下のようになるかと思います。

class Message < ApplicationRecord
  belongs_to :employer
  belongs_to :employee
  # メッセージを送るモデルが増えたら、それに従いbelongs_toの数も増えていく
end

class Employer < ApplicationRecord
  has_many :messages
end

class Employee < ApplicationRecord
  has_many :messages
end

messagesテーブルにはemployer_idカラムとemployee_idカラムがあり、それを使って各モデルとJOINすることになります。
実際のコード内での呼び出し方は以下のようになるでしょう。

employer = Employer.find(params[:employer_id])
employer.messages # select * from messages where employer_id = ?

message = Message.find(params[:message_id])
message.employee # select * from employees where employee_id = ?

ただ、この書き方は上記ダックタイピングNG例と同じような問題をはらんでおり、message.employermessage.employeeのどちらが呼べるのかは都度チェックする処理を挟まないとメッセージ送信者モデルを取得できません。
またさらにデータベースが絡むことにより「メッセージ送信者モデルを増やす際にはxx_idカラムを増やすためにmessagesに都度ALTER TABLEを行う必要がある」といった問題もプラスされています。

こういった時にダックタイピングを使って簡潔に書けるようにするもの、それがポリモーフィックです

ポリモーフィックの使いどころ/便利なところ

上記のまとめになりますが、Railsのポリモーフィックが便利なのは「一つのモデルを同じインターフェースを持ったものが扱う(ダックタイピングする)」場合になります。

実際にポリモーフィックを利用したコードは以下のようになります。

# 注:以下のcompany_name, last_name, first_nameは各テーブルに定義されたカラムだと思ってください。

class Message < ApplicationRecord
  belongs_to :messageable, polymorphic: true
end

class Employer < ApplicationRecord
  has_many :messages, as: :messageable

  def sender_name
    company_name
  end
end

class Employee < ApplicationRecord
  has_many :messages, as: :messageable

  def sender_name
    "#{last_name} #{first_name}"
  end
end

このコードは以下のように使うことができます。

# messageableを通すことで、EmployerとEmployeeのどちらであるかを意識する必要がない。
# また、messageableなモデルのインターフェースとして必ず`#sender_name`が定義されているので、メソッドが定義されているかどうかのチェックも必要ない。
Message.find(params[:id]).messageable.sender_name

# メッセージ送信者モデル側からは通常のhas_many関連として扱える
Employer.find(params[:id]).messages
Employee.find(params[:id]).messages

ダックタイピングを使うことによって、メッセージ送信者モデル(messageable)の実際のクラスを意識せずに#sender_nameにアクセスすることができました。

また、messageableなモデルを追加する時にも以下のように追加モデルを書くだけでよく、既存コードをいじる必要はありません。

class Guest < ApplicationRecord
  has_many :messages, as: :messageable

  def sender_name
    "ゲスト"
  end
end

これこそが、ポリモーフィックの便利なところです

よくあるポリモーフィックの書き方

以上でポリモーフィックの概念レベルの説明は終わりとなりますが、ついでによくあるポリモーフィックの書き方も書いておきます。

class Message < ApplicationRecord
  belongs_to :messageable, polymorphic: true
end

# インターフェースを明確化するために、moduleで固める
module Messageable
  extend ActiveSupport::Concern

  included do
    has_many :messages, as: :messageable
  end

  def sender_name
    # オーバーライドされなかった場合はエラーが上がるようにしておく
    raise NotImplementedError
  end

  def sender_email
    raise NotImplementedError
  end
end

class Employer < ApplicationRecord
  # Messageを使うポリモーフィックなモデルはMessageableをincludeする
  include Messageable

  # moduleで定義されているメソッドをオーバーライドする
  def sender_name
    company_name
  end

  def sender_email
    "admin@example.com"
  end
end

class Employee < ApplicationRecord
  include Messageable

  def sender_name
    "#{last_name} #{first_name}"
  end

  def sender_email
    email
  end
end

間違ったポリモーフィック

ダックタイピングNG例とかぶりますが、これまでを踏まえて「こういうポリモーフィックは間違っている」ということを説明します。

例えば、以下のようなコードがあるとします。

messageable = Message.find(params[:id]).messageable

# インターフェースが統一されていないと、クラスで条件分岐する必要がある。
case messageable
when Employer
  messageable.employer_email
when Employee
  messageable.employee_email
end

# もしくは、respond_to?でメソッドが定義されているかで分岐する
if messageable.respond_to?(:employer_email)
  messageable.employer_email
elsif messageable.respond_to?(:employee_email)
  messageable.employee_email
end

このようにmessageableの実態・インターフェースを逐一調べるようになってしまうとポリモーフィックの意味がないどころか、抽象化レイヤが入る分複雑度が増し、非常に読みづらくテストが書きづらくバグりやすいコードになってしまいます。
ポリモーフィックを使う場合は必ずインターフェースを統一し、すべてのクラスを同じように扱えるようにしなければなりません。

まとめ

  • インターフェースとは振る舞い・入出力の定義
  • ダックタイピングとは、複数のクラスのインターフェースを統一し、同じように扱えるようにすること
    • コードを簡潔に、拡張性を保って書くことができる
  • ポリモーフィックとはダックタイピングの一種

雑談

以下、とりとめのない話です

個人的にはポリモーフィック自体は非常に便利だけどもRubyのシンタックス的に厳しいものがあるなー、と思っています。

理由は2つ

  1. あるインターフェースを実装することを強制できない
    • 必須メソッドが実装されるかは実装者次第
    • Javaで言うところのabstract/interfaceが欲しい
  2. オーバーライドした時に、どのメソッドがオーバーライドしたものなのかわかりにくい

2の観点に関してもう少し説明すると、まず以下のような感じにMessageableなmoduleがあったとして、

module Messageable
  extend ActiveSupport::Concern

  included do
    has_many :messages, as: :messageable
  end

  def sender_name
    raise NotImplementedError
  end
end

個人的にはmoduleのメソッドのオーバーライドを明示的にするため、includeする際にブロックを使ってその中でオーバーライドさせたいのですが、残念ながらできません。(シンタックスエラーにはならないですが、オーバーライドはされません)

class Employer < ApplicationRecord
  # includeにブロックを書いて、その中でオーバーライドしたい
  include Messageable do
    def sender_name
      "#{last_name}様"
    end
  end
end

なので、以下のようにincludeとオーバーライドは分ける必要があるのですが、コード量が多くなってくるとどれがオーバーライド用のメソッドでどれがそれ以外のメソッドなのかわかりにくくなるので、何か一工夫が欲しくなります。

class Employer < ApplicationRecord
  include Messageable

  # このメソッドはEmployerに普通に定義されたメソッドなのか、それともMessageableのオーバーライドなのか見た目ではわからない
  def sender_name
    "#{last_name}様"
  end
end

メソッド群に名前を与えたいだけならば妥協案としては以下のようにオーバーライド用メソッドをmoduleに包んで即includeという手もあるのですが、冗長さは否めません

class Employer < ApplicationRecord
  include Messageable

  # moduleで包んで
  module OverrideMessageable
    def sender_name
      "#{last_name}様"
    end
  end
  # 即include
  include OverrideMessageable
end

トリッキーな手としてはModule.newを使う方法もあるのですが、読みにくいしmoduleの中でincludedが使えなくなるしでやはり微妙です

class Employer < ApplicationRecord
  # do..endで書くとModule.newの引数でなくincludeの引数となるっぽくてオーバーライドされない
  include Module.new {
    include Messageable 

    # Messageableの中に`included do; has_many :foo; end`があると、
    # Module.newの中で`has_many :foo`を実行して`has_many`を呼べずに死ぬ

    def sender_name
      "#{last_name}様"
    end
  }
end

このようなRubyのシンタックス上の限界からRailsのポリモーフィック関連は実装者の実力に依存しがちな構文に思えるので、利用する際には細心の注意を払う必要があるでしょう。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした