経緯
先月、子育てで仙台に引っ越してから初めてのLT発表をしました。RailsのDelegated Types
について、実際の使用経験をもとに3つのポイントを整理して発表させていただきました。
初参加でしたが、Sendai.rbの皆さんには温かく迎えていただき、とても良い体験でした。
スライド
スライドの補足
想定する要件
- 営業支援サービスを作る
- toB, toC両方を対象にしたい
- 顧客として個人、法人を対象にする
- 顧客共通(Customer): 住所(address)
- 個人(Person): 名(first_name), 姓(last_name)
- 法人(Corporation): 会社名(company_name), 法人番号(tax_identification_number)
- 顧客を一括管理、一覧できるようにしてほしい
実装パターンの比較検討
よくある実装として3つのパターンを挙げました
1. STI(Single Table Inheritance)
- 一つのテーブルに全ての属性を詰め込む
- pros: 一括操作が可能
- cons: NULL値の多発、テーブル肥大化
2. テーブル分離(CCI: Concrete Class Inheritance)
- 法人用、個人用に完全にテーブルを分ける
- pros: 正規化されている
- cons: 一括操作が困難、カラム重複
3. Delegated Types
- 共通項目用のテーブルと固有項目用のテーブルをpolymorphic関連で接続
- pros: 一括操作可能、正規化、型安全
- cons: 外部キー制約の設定が困難
Delegated Types理解の2つの文脈
発表で強調したのは、Delegated Typesを理解するには2つの文脈が重要だということです。
文脈1: Polymorphic関連の改良版
従来のpolymorphic関連では以下の問題がありました:
- eager loadingができない
-
customable_type
のvalidationを自分で書く必要がある - 型安全なアクセスができない
Delegated Typesはこれらを解決し、より使いやすい形でpolymorphic関連を提供しています。
文脈2: CTI(Class Table Inheritance)を「継承より委譲」で実装
CTIのテーブル設計とほぼ同じですが、重要な違いがあります:
- CTI: 親テーブルと子テーブルでIDを共有
- Delegated Types: 親テーブル側にtype, idカラムを持つ(polymorphic関連)
コード上でも大きな違いがあります:
- STI/CTI: PersonやCorporationインスタンスを主に扱う
- Delegated Types: Customerインスタンスを主に扱う
この違いにより、Delegated Typesでは拡張が簡単になります。例えば「見込み客(Prospect)」のような概念が追加されても、継承ツリーを考える必要がありません。
includeよりWrapper(PORO)パターン
ドキュメントでよく見るinclude
パターンは、以下の問題があります:
- include先のクラスが肥大化する
- メソッド名の衝突リスク
代わりに、Wrapperパターンを推奨しています:
class Customer < ApplicationRecord
SOURCES = {
person: Customable::Person,
corporation: Customable::Corporation
}
private
def source
@source ||= SOURCES[customable_type.underscore.to_sym].new(customable)
end
end
このパターンの利点:
- FatModelを避けられる
- 名前空間を圧迫しない
- publicメソッドのみに依存するため安定する
抽象化を貫く重要性
最も重要なのは、親クラスやViewに詳細を漏らさないことです。
❌ 悪い例:
<% if customer.person? %>
<%= customer.person.first_name %>様
<% else %>
<%= customer.corporation.company_name %>御中
<% end %>
✅ 良い例:
<!-- 統一インターフェース -->
<%= customer.formal_name %>
<!-- 動的パーシャル選択 -->
<%= render "customers/customables/#{customer.customable_type}", customer: customer %>
中途半端な抽象化は技術的負債に直結するため、強い意志で抽象化を貫くことが重要です。
まとめ
Delegated Typesを効果的に使うための3つのポイント:
- 2つの文脈で理解する: Polymorphic関連の改良とCTIの委譲実装
- includeよりWrap: 依存を安定させ、FatModelを避ける
- 抽象化を貫く: 詳細を漏らさない強い意志