Rails Single Table Inheritance and DRY Code (日本語)

Table of contents

STI(Single Table Inheritance/単一テーブル継承)をRailsでどう実装するかについて、情報をまとめて残したいと思います。

STIの概要

  • 英語:Single Table Inheritance
  • 日本語:単一テーブル継承

OOPプログラミングの世界でよく知られている「継承」技術の一つです。

共通項目、共通振る舞いを折り出してスーパークラスを作り、 非共通項目や振る舞いはそのスーパークラスの継承したサブクラスで定義するというやり方ですね。

こうすることで、コードの重複を防げます。メンテナンスもやりやすいコードになります。

データベースの設計にも適用しようというのがSTIです。 特に、モデル間の共通データがほとんどだが、振る舞いが異なる場合はSTIが一番適切です。

RailsでSTIを実装してみる

例のシステム

  • スマホを販売している会社がスマホのデータを管理したい
  • スマホの種類は、iPhoneとAndroidの2種類がある
  • 種類に関係なく、製造番号やメモリや画面サイズなどのデータが共通となる(→ STIに適切ヒントですね
  • なお、スマホの種類によって異なるアクションができるようにする必要がある。 例えば、スマホの最新OSバージョンチェック機能があるとして、
    • iPhoneの場合AppleのAPIを叩く必要がある、
    • 対して、Androidの場合GoogleのAPIを叩かないといけない

Rails実装

何にも気にしないで作ると、iphone/androidそれぞれのテーブル、モデルを作成しますよね。 そして、共通のデータやメソッドもあるので、 似たような記述・コピペが増えそうな予感を感じますよね!

それを避ける為に、STIを適用してみしょう。 ↓の図をご覧ください。

STI Rails

一見どうということもないように見えますが、実は

  • iPhoneとAndroidテーブルは存在しない
  • 全てのスマホのデータが、smartphoneテーブル一つだけに保存される

スーパーテーブルを用意して、継承のサブテーブルを作成するということです。

データレベルでは、スマホのデータしか実在しませんが、 アプリケーションレベル(Rails上)では、スマホを継承したiPhoneとAndroidモデルが存在します。

Railsで実際に実装する

1. マイグレーションを作成

スマホのテーブルしか存在しないので、マイグレーションファイルは1個になります。

class CreateSmartphoneTables < ActiveRecord::Migration[5.2]
  def change
    create_table :smartphones do |t|
      t.string :type, null: false
      t.string :serial_no
      t.float :screen_size
      t.integer :memory
      t.timestamps null: false
    end
  end
end

ポイントは、typeカラムを必須で用意する必要があることです。 RailsにSTIを使っているよって伝えるためです。

後で、このテーブルの実データを確認してみると、 type カラムに Smartphone::iPhoneSmartphone::Android の値が入ってることが分かります。

2. モデルクラスを実装

次に、モデルクラスを実装しましょう。

最初は、Smartphoneモデルですね。通常の形で実装するだけです。

class Smartphone < ApplicationRecord
  # 共通バリデーション
  validates :serial_no, presence: true

  # 共通メソッド
  def say_hi
    puts "Hi"
  end

  # 後で定義する
  def latest_os_version
    raise NotOverrideError
  end
end
  • 共通のバリデーションやメソッドはここで定義しましょう。
  • また、先ほど説明した type カラムは、もし別のカラムを使いたい場合はこれで変えられます。
class Smartphone < ApplicationRecord
  self.inheritance_column = :smartphone_class # ここでinheritance_columnのカラムを指定
end

そして、やっとIphoneとAndroidモデルを実装してみましょう。

class Smartphone
  class Iphone < Smartphone
    # 再定義する
    def latest_os_version
      call_APPLE_api 🍎
      parse_response
      ...
    end
  end
end
class Smartphone
  class Android < Smartphone
    # 再定義する
    def latest_os_version
      call_GOOGLE_api 🤔
      parse_response
      ...
    end
  end
end

気をつけないといけないのは、IphoneやAndroidクラスが継承するのはApplicationRecordでなく親であるSmartphoneですね。

これで、親で設定したリレーションやバリデーションやメソッドが全部子クラスにも引き継がれます。 なお、latest_os_versionメソッドの振る舞いはそれぞれ異なって実装することができますね!

DRY(重複排除)コードを目指すなら、適用できる場合はSTIを使いましょう。

Share on:
comments powered by Disqus