Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rails Delegated Types を使いこなすための3つのポイント

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Iwaide Iwaide
July 26, 2025

Rails Delegated Types を使いこなすための3つのポイント

Rails 6.1で導入されたDelegated Typesは、STIの課題を解決する強力な機能ですが、単なるpolymorphic関連の改良版以上の価値があります。

本セッションでは、Delegated Typesを実際のプロダクトで効果的に活用するための
3つの重要なポイントを解説します:


2つの文脈を理解する - Polymorphic関連の改良とCTI(Class Table Inheritance)を移譲で実現したパターンであることを理解し、適切な設計判断を行う


includeよりWrap - モジュールのincludeではなくPOROを使ったラッパーパターンで、Fat Modelを避け依存関係を安定させる


抽象化を貫く - 親クラスやViewに実装詳細を漏らさず、統一されたインターフェースを維持する

個人・法人顧客を扱う営業支援システムを例に、STI・CTI・Delegated Typesの比較から実装のベストプラクティスまで、実践的な内容をお届けします。

Avatar for Iwaide

Iwaide

July 26, 2025

Other Decks in Technology

Transcript

  1. 自己紹介 Iwaide 株式会社オーバードライブ所属 受託開発 + 自社開発( ボドゲーマ) 去年11 月に子育てのために仙台へ移住 インフラ〜バックエンド中心

    趣味: 漫画・音楽( 作曲・ギター) ・お茶 ボドゲーマ ボードゲームのレビュー・購入・委託 ボドゲカフェの検索 ボドゲ会の開催 などができるボードゲームの総合プラットフォーム
  2. 考えてみましょう 営業支援サービスを作る toB, toC 両方を対象にしたい 顧客として個人、法人を対象にする 顧客共通(Customer): 住所(address) 個人(Person): 名(first_name),

    姓(last_name) 法人(Corporation): 会社名(company_name), 法人番号(tax_identification_number) 顧客を一括管理、一覧できるようにしてほしい 下記のような要望が来たときどういうクラス設計、テーブル設計を行いますか?
  3. STI (単一テーブル継承) customers int id PK string type STI 識別子

    string address 共通 string first_name Person 用 string last_name Person 用 string company_name Corporation 用 string tax_identification_number Corporation 用 datetime created_at type カラムにPerson, Corportation を入 れておく ✅: 一括操作可能 ❌: NULL 値多発 ❌: テーブル肥大化 実装パターン1
  4. 分離パターン(CCI ) corporations int id PK string company_name string tax_identification_number

    string address datetime created_at people int id PK string first_name string last_name string address datetime created_at Corportation, Person に 対応するテーブルのみ作 成する ✅: 正規化されている ❌: 一括操作が困難 ❌: テーブル間で重複するカ ラムが発生 実装パターン2
  5. DelegatedTypes Customer と Corporation, Person を polymorphic 関連で繋ぐ ✅ 一括操作可能

    ✅ 正規化 ✅ 型安全 ❌ 外部キー制約 実装パターン3 customable customable customers int id PK string customable_type polymorphic int customable_id polymorphic string address 共通項目 datetime created_at people int id PK string first_name string last_name corporations int id PK string company_name string tax_identification_number
  6. Delegated Types とは? Rails 6.1 で導入された、polymorphic な関連を型安全に扱える仕組み class Customer <

    ApplicationRecord delegated_type :customable, types: %w[Person Corporation] delegate :display_name, to: :customable end module Customable included do has_one :customer, as: :customable end def display_name raise NotImplementedError, "You must implement #{self.class}##{__method__}" end end class Person < ApplicationRecord include Customable def display_name "#{last_name} #{first_name}" end end class Corporation < ApplicationRecord include Customable def display_name company_name end end
  7. 文脈1: Polymorphic 関連の改良 従来の問題 Delegated Types で改善 1. 2 つの文脈を理解する

    belongs_to :customable, polymorphic: true # ❌ eager loadingが困難 Customer.includes(:customable) # できない # ❌ validationが必要 validates :customable_type, inclusion: { in: %w[Person Corporation] } # ❌ 型安全性なし customer.customable.first_name # Person以外だとエラー delegated_type :customable, types: %w[Person Corporation] # ✅ eager loading対応 Customer.includes(:customable) # できる! # ✅ ユーザー定義のvalidation不要 # validates :customable_type, # inclusion: { in: %w[Person Corporation] } # ✅ 型安全なアクセサ customer.person? # true/false customer.person.first_name # 型安全
  8. 文脈2: CTI を意識しつつも 継承より移譲 をベースにした実装パターンである 例: STI を用いた場合 customers int

    id PK string type STI 識別子 string address 共通 string first_name Person 用(Corporation では不要) string last_name Person 用(Corporation では不要) string company_name Corporation 用(Person では不要) string tax_identification_number Corporation 用(Person では不要) datetime created_at ✅: 一括操作可能 ❌: NULL 値多発 ❌: テーブル肥大化 1. 2 つの文脈を理解する
  9. CTI(ClassTableInheritance) id=id id=id customers int id PK string address datetime

    created_at people int id PK string first_name string last_name corporations int id PK string company_name string tax_identification_number クラスごとにテーブルを分割する customer.id == person.id, corporation.id Delegated Types customable customable customers int id PK string customable_type polymorphic int customable_id polymorphic string address 共通項目 datetime created_at people int id PK string first_name string last_name corporations int id PK string company_name string tax_identification_number id を共有ではなく、customers 側に customable_type, customable_id を持たせる 1. 2 つの文脈を理解する
  10. 継承より移譲 ⚠️: メインで扱うクラスが異なる STI, CTI: Person, Corporation DelegatedTypes: Customer 1.

    2 つの文脈を理解する customer = Customer.find_by(customable_type: 'Person') # => Personインスタンスが返る customer = Customer. find_by(customable_type: 'Corporation') # => Corporationインスタンスが返る customer = Customer.find_by(customable_type: 'Person') # => Customerインスタンスが返る customer = Customer. find_by(customable_type: 'Corporation') # => Customerインスタンスが返る
  11. 🙆‍♂️ 移譲は継承より拡張が簡単 例:Prospect (見込み客)のような概念が現れたとき ❌ 継承(STI) CTI の場合でも継承ツリーにProspect を入れるか などを考える必要がでてくる

    ✅ 移譲(DelegatedTypes) 1. 2 つの文脈を理解する # ProspectはCustomerにスコープとして定義する?メソッドは? class Customer; end class Person < Customer; end class Corporation < Customer; end # Customerとは別のクラスとして追加できる class Prospect < ApplicationRecord delegated_type :prospectable, types: %w[Person Corporation] end class Person < ApplicationRecord include Customable include Prospectable has_one :customer, as: :customable has_one :prospect, as: :prospectable end
  12. include パターン ❌: include 先のクラスが肥大化する ❌: 名前空間によってメソッド名が衝突する 2. include よりWrap

    class Customer < ApplicationRecord delegated_type :customable, types: %w[Person Corporation] delegate :display_name, :formal_name, to: :customable end module Customable included do has_one :customer, as: :customable end def formal_name "#{display_name} #{title}" end # abstract def display_name; end def title; end end class Person < ApplicationRecord include Customable has_one :customer, as: :customable # override def display_name "#{last_name} #{first_name}" end # ⚠️: 名前がシンプルすぎて他と被りそう # ⚠️: もしくは別のところから参照されてしまうかも? def title "様" end end
  13. Wrap パターン 間に1 層設けることで… ✅: FatModel を避けることができる ✅: 名前空間を圧迫しない ✅:

    public メソッドのみに依存するため安定する 2. include よりWrap uses uses wraps wraps Customer +delegated_type customable +formal_name() Person +first_name: String +last_name: String Corporation +company_name: String Customable::Person -Person person +formal_name() -title() -display_name() Customable::Corporation -Corporation corporation +formal_name() -title() -display_name()
  14. Wrap パターンの実装例 2. include よりWrap class Customer < ApplicationRecord delegated_type

    :customable, types: %w[Person Corporation] delegate :display_name, :formal_name, to: :source SOURCES = { person: Customable::Person, corporation: Customable::Corporation } private def source @source ||= SOURCES[customable_type.underscore.to_sym] new(customable) end end # app/models/customable/person.rb module Customable class Person def initialize(person) @person = person end def formal_name "#{title} #{display_name}" end def display_name "#{@person.last_name} #{@person.first_name}" end private def title "様" end end end
  15. 親クラスやView に詳細が漏れているとあっという間に負債化する ❌ 詳細が漏れている ✅ 抽象化を貫く 3. 抽象化を貫く <% #

    親クラスやViewがサブクラスの詳細を知っている %> <% if customer.person? %> <p>個人のお客様</p> <%= customer.person.first_name %>様 <% else %> <p>法人のお客様</p> <%= customer.corporation.company_name %>御中 <% end %> <% # 親クラスは抽象化されたインターフェースのみ使用 %> <!-- 統一インターフェース --> <%= customer.formal_name %> <!-- 動的パーシャル選択 --> <%= render "customers/customables/#{customer.customable_type}", customer: customer %>
  16. DelegatedTypes を使いこなすための3 つのポイント 1. 2 つの文脈で理解 Polymorphic 関連の改良版 CTI を移譲で実現

    2. include より Wrap 間に一層設けることで依存を 安定させる 名前空間を圧迫しない 3. 抽象化を貫く 親クラスに詳細を漏らさない 中途半端な実装は負債化する