🎒

1つの設定を複数モデルに分散させるRailsパターン

に公開

はじめに

Railsアプリを開発している中で、「あるモデルの設定を、関連する複数のモデルに自動で反映させたい」 という課題に直面しました。
そこで使用した実装方法を、学習塾システムを題材に、「クラス管理者が生徒の通知設定を一括で管理できる仕組み」を実装するパターンとして紹介します。
この方法を用いることで、保守性・拡張性の高い「設定の継承」をシンプルに実現できます。

前提

サービス概要

以下のようなシステムを想定します:

  • 学習塾で利用されるシステム
  • 各生徒に対して「宿題リマインダー通知」や「お知らせ通知」を行える
  • クラスごとに通知方針が異なる(受験クラスは通知多め、自習重視のクラスは通知少なめ等)

モデル定義

学習塾(CramSchool)および塾のクラス(Classroom)

学習塾には複数のクラスがあり、クラスには複数の生徒が在籍しています。
クラスには、その特徴に応じた通知のデフォルト設定があります。

# 学習塾
class CramSchool < ApplicationRecord
  has_many :classrooms
  has_many :students, through: :classrooms
end

# 塾のクラス
class Classroom < ApplicationRecord
  belongs_to :cram_school
  has_many :students
  has_one :default_setting

  # クラス作成と同時に、デフォルト設定も作成する
  after_create :create_default_setting!
end

塾の生徒(Student)

必ずどこか1つのクラスに在籍しています。
生徒ごとに、「SMS通知」と「Email通知」のON/OFFを行えます。

class Student < ApplicationRecord
  belongs_to :classroom
  has_one :cram_school, through: :classroom
  has_one :sms_setting # 生徒ごとのSMS通知設定
  has_one :email_setting # 生徒ごとのEmail通知設定
end

クラスごとのデフォルト設定(DefaultSetting)

class DefaultSetting < ApplicationRecord
  belongs_to :classroom

  # 各通知タイプのデフォルト設定(Boolean型のカラム)
  # - sms_homework_reminder(宿題リマインダーのSMS通知)
  # - email_homework_reminder(宿題リマインダーのEmail通知)
  # - sms_announcement(生徒へのお知らせのSMS通知)
  # - email_announcement(生徒へのお知らせのEmail通知)
end

生徒ごとのSMS通知設定

class SmsSetting < ApplicationRecord
  belongs_to :student

  # Boolean型のカラム
  # - homework_reminder(宿題リマインダー)
  # - announcement(生徒へのお知らせ)
end

生徒ごとのEmail通知設定

class EmailSetting < ApplicationRecord
  belongs_to :student

  # Boolean型のカラム
  # - homework_reminder(宿題リマインダー)
  # - announcement(生徒へのお知らせ)
end

課題

クラスの管理者は、クラスごとの通知のデフォルト設定を管理できます。このデフォルト値を、新しい生徒がクラスに入った時に自動適用する必要があります。

しかし、通知方法が複数(SMS、Email)あり、それぞれ別々のモデルで管理している場合、単純に実装すると以下の問題が生じます:

  • SMS/Email以外の通知が増えると、コードをコピペして修正箇所が増える
  • カラム追加のたびにこのメソッドを修正する必要がある
  • 他の箇所でもデフォルト設定を参照する場合は、そこも修正が必要
def apply_default_setting
  default_setting = classroom.default_setting

  # SMS設定を個別に指定
  create_sms_setting!(
    homework_reminder: default_setting.sms_homework_reminder,
    announcement: default_setting.sms_announcement
    # 他の通知タイプも同様に追加が必要
  )

  # Email設定を個別に指定
  create_email_setting!(
    homework_reminder: default_setting.email_homework_reminder,
    announcement: default_setting.email_announcement
    # 他の通知タイプも同様に追加が必要
  )
end

この課題を解決するのが、以下で紹介する「マッピング」を用いた実装パターンです。

実装パターン

1. マッピング定数の定義

各設定モデルに、自身の属性とデフォルト設定の属性の対応マップを定義します。

class SmsSetting < ApplicationRecord
  belongs_to :student
  
  # 自分がもつ属性と、それに対応するデフォルト設定の属性名のマッピング
  DEFAULT_SETTING_MAP = {
    "homework_reminder" => "sms_homework_reminder",
    "announcement" => "sms_announcement"
  }.freeze
end

class EmailSetting < ApplicationRecord
  belongs_to :student
  
  DEFAULT_SETTING_MAP = {
    "homework_reminder" => "email_homework_reminder",
    "announcement" => "email_announcement"
  }.freeze
end

2. デフォルト設定を適用するメソッドの実装

各設定モデルに、DefaultSettingから設定用パラメータを生成するクラスメソッドを作成します。
ポイント:

  • transform_valuesメソッドを用いることで、マッピングのvalues部分だけを変換
  • sendメソッドを用いることで、動的にDefaultSettingの属性を参照
class SmsSetting < ApplicationRecord
  # 既存のコード...
  
  def self.build_for_default(default_setting:)
    DEFAULT_SETTING_MAP.transform_values do |default_attr| 
      default_setting.send(default_attr) 
    end
    # 実行例: { "homework_reminder" => true, "announcement" => false }
  end
end

class EmailSetting < ApplicationRecord
  # 既存のコード...

  def self.build_for_default(default_setting:)
    DEFAULT_SETTING_MAP.transform_values do |default_attr| 
      default_setting.send(default_attr) 
    end
    # 実行例: { "homework_reminder" => true, "announcement" => true }
  end
end

3. 設定反映の実装

Studentモデルに、デフォルト設定を個々の通知設定に適用するメソッドを実装します。after_createコールバックを用いて、Studentが作成されたタイミングで自動的にデフォルト設定を適用させます。

class Student < ApplicationRecord
  belongs_to :classroom
  has_one :sms_setting
  has_one :email_setting
  
  after_create :apply_default_setting
  
  private

  # デフォルト設定を適用する
  def apply_default_setting
    default_setting = classroom.default_setting
    
    # SMS設定の作成&デフォルト設定を適用
    # **を使ってハッシュを展開
    create_sms_setting!(**SmsSetting.build_for_default(default_setting:))
    
    # Email設定の作成&デフォルト設定を適用
    create_email_setting!(**EmailSetting.build_for_default(default_setting:))
  end
end

使用例

# 学習塾の作成
cram = CramSchool.create!(name: "進学塾ABC")

# 塾のクラスの作成
classroom = cram.classrooms.create!(name: "大学進学コース")

# クラスのデフォルト設定を更新
classroom.default_setting.update!(
  sms_homework_reminder: true,
  email_homework_reminder: true,
  sms_announcement: false,
  email_announcement: true
)

# 新しくクラスに入る生徒の作成
student = classroom.students.create!(name: "田中太郎")

# 生徒の通知設定が、自動的にクラスのデフォルト設定に反映されていることを確認
puts student.sms_setting.homework_reminder     # => true
puts student.email_setting.homework_reminder   # => true
puts student.sms_setting.announcement          # => false
puts student.email_setting.announcement        # => true

このパターンの利点

保守性の向上

新しい通知タイプ(定期テスト通知、面談予約通知等)を追加する場合でも、各通知モデルのDEFAULT_SETTING_MAPに項目を追加し、対応するカラムをマイグレーションで追加するだけで済みます。

class SmsSetting < ApplicationRecord
  DEFAULT_SETTING_MAP = {
    "homework_reminder" => "sms_homework_reminder",
    "announcement" => "sms_announcement",
    "exam_reminder" => "sms_exam_reminder", # 例:定期テストのSMS通知を追加
  }.freeze
end

一貫性の確保

すべての通知設定が同じパターンで管理されるため、コードの構造が統一され、理解しやすくなります。実装者以外のメンバーでも、一度パターンを理解すれば、他の設定についても容易に把握できそうです。

拡張性の向上

新しい通知方法(例:LINE通知、Slack通知、アプリ内プッシュ通知等)を追加する場合も、同じ仕組みを使って簡単に対応できます。

# 例:LINE通知を導入する場合
class LineSetting < ApplicationRecord
  belongs_to :student
  
  DEFAULT_SETTING_MAP = {
    "homework_reminder" => "line_homework_reminder",
    "announcement" => "line_announcement"
  }.freeze
  
  def self.build_for_default(default_setting:)
    DEFAULT_SETTING_MAP.transform_values { |default_attr| 
      default_setting.send(default_attr) 
    }
  end
end

おわりに

今回紹介したように、

  • 定数によるマッピング
  • メタプログラミングによるシンプルな処理
    を組み合わせることで、1つのマスター設定を複数モデルに効率よく反映できます。
    学習塾システム以外にも、様々なドメインで応用できるパターンですので、ぜひ参考にしてみてください!

Discussion