ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

テンプレートメソッドパターンで Rails コントローラを DRY にする

こんにちは!アジャイル事業部の @maimuです。

この記事は ESM Advent Calendar 2025 の16日目の記事です。 Rails のコントローラでテンプレートメソッドパターンと Delegated Types を組み合わせてコードを DRY にすることを書いたのですが、社内のブログレビューでも議論を巻き起こした内容です・・・!

ブログ用にサンプルコードを書いたため、テンプレートメソッドパターンを使うためのコードになってしまっている節はありそうですが、現在の自分が学んだことのアウトプットとして公開しようと思います。

はじめに

お仕事の実装でコードがちょっとずつ似ているけど違う部分もあって、それを共通化するか?しないか?ということを考える機会が最近よくありました。その際、コントローラの実装でデザインパターンのテンプレートメソッドパターンを使って実装を共通化する方法を知りました。

デザインパターンについては過去に本を読んだことはあったものの、自分が実際に使うレベルまでは意識が及んでいなくて、今回の経験が初めて実装とデザインパターンが結びついた瞬間でした。

現在参加しているプロジェクトでは設計について考える機会も多く、改めてデザインパターンの本を読み直すとともに、実装する際にも「何を・どう」作るかを以前よりも意識するようになりました。

今回はそんなきっかけを作ってくれたテンプレートメソッドパターンについて、サンプルコードとともに紹介し、実装で使用する際のポイントをまとめていきたいと思います。

チャットアプリを題材に考える

今回は、チャットアプリを例に

  • 通知を受け取る頻度
  • 通知を受け取らない時間帯
  • 通知を受け取りたい特定のキーワード

を Rails で実装する例を簡易的に考えたいと思います。 3つの設定は初回設定後も変更が可能とします。

通知に関して、頻度・時間帯・キーワードを設定したいという内容ですが、設定として受け取るパラメータの値はそれぞれ異なります。リソースは3つ必要で、それぞれに対してコントローラが必要になりそうです。コントローラで定義するアクションは createupdate が必要そうだな・・・と考えていきます。

ここでコントローラの createupdate アクションでやりたいことは何かを考えると、それぞれの設定内容を「保存・更新」することです。これは3つのリソースともに共通していると考えられます。

ここまでをまとめると

  • 3つの設定は受け取るパラメータの値の数や構造が異なる
  • 設定内容の保存と更新が必要な点が共通している

となります。

共通している部分が見つかったならば、実装をまとめる方法を考えたいです。そんな時に選択肢の一つとして活用できるのが、今回のブログテーマである「テンプレートメソッドパターン」です。

※選択肢の一つとして書いているのは、テンプレートメソッドパターンでなければ解決できない!という内容ではないことを補足しておきます。

テンプレートメソッドパターンで実装を進めてみる

テンプレートメソッドパターンとはデザインパターンの一つで、振る舞いの枠組みを基底クラスで定義し、具体的内容をサブクラスで実装します。

それでは、今回のチャットアプリの例をコードで見ていきたいと思います。

一番初めに考える点として、テンプレートメソッドパターンの基底クラスでオブジェクト作成の共通化をどう実現するかです。 パッと思い浮かぶのがポリモーフィック関連づけですが、自由度が高くどこまでが対象なのかが追いにくくなりそうな点が気になります。 今回、サブクラスは3つであることが前提となっているため、ポリモーフィック関連ではなく Delegated Types も良さそうです。 Delegated Types とは共通の振る舞いを持つ複数モデルを、1つの親モデルで扱うための仕組みです。Delegated Types を活用すれば、どのモデルが対象なのかを明示的に列挙できるため、今回はこちらとテンプレートメソッドパターンの組み合わせで進もうと思います。

railsguides.jp

Delegated Types については、それだけで一つのブログ記事が書けてしまう内容になるため、今回の記事では必要な手順のみに触れ、詳細な言及は省略します。

まず、3つの設定は「通知」に関わるものであるため、notification_settings テーブルを作成し、settable_idsettable_type カラムを用意します。

スーパークラスとして機能する NotificationSetting モデルを定義します。

# app/models/notification_setting.rb

class NotificationSetting < ApplicationRecord
  delegated_type :settable, types: %w[
    NotificationSetting::FrequencySetting
    NotificationSetting::QuietHoursSetting
    NotificationSetting::KeywordSetting
  ], dependent: :destory
end

次は Settable モジュールを定義します。

module NotificationSetting::Settable
  extend ActiveSupport::Concern

  included do
    has_one :notification, as: :settable, dependent: :destroy
  end
end

このモジュールを3つの設定と対応するサブクラスに include します。

class NotificationSetting::FrequencySetting < ApplicationRecord
  include NotificationSetting::Settable
end
class NotificationSetting::QuietHoursSetting < ApplicationRecord
  include NotificationSetting::Settable
end
class NotificationSetting::KeywordSetting < ApplicationRecord
  include NotificationSetting::Settable
end

こうすることで以下のように NotificationSetting オブジェクトを作成する際に、サブクラスを同時に指定できるようになります。

current_user.notification_settings.create!(settable: FrequencySetting.new(frequency: 'daily'))

このオブジェクトの作成方法をテンプレートメソッドパターンで基底クラスに共通処理として持たせて実装を進めていきます。

次はリソースの定義を見ていきます。

# routes.rb

Rails.application.routes.draw do
  namespace :notifications do
    resources :frequency_settings,   only: %i[create update]
    resources :quiet_hours_settings, only: %i[create update]
    resources :keyword_settings,     only: %i[create update]
  end
end

3つの設定は「通知」に関わるものであるため、ネームスペース notifications 配下にリソースを定義します。 要件の部分で確認した通り、 createupdate アクションが共通しています。

続いて、共通しているアクションと振る舞いの枠組みを定義する基底コントローラを作成します。

# app/controllers/notifications/base_controller.rb
  class Notifications::BaseController < ApplicationController
    def create
      @setting = current_user.notification_settings.create!(
        settable: settable_class.new(settable_attrs)
      )

      render :show, status: :created
    end

    def update
      @setting = current_user.notification_settings
        .where(settable_type: settable_class.name)
        .find(params[:id])

      @setting.settable.update!(settable_attrs)

      render :show
    end

    private

    def settable_class  = raise 'not implemented'
    def settable_params = raise 'not implemented'

    def settable_attrs
      setting_params[:settable]
    end

    def setting_params
      params.expect(notification_setting: [
        settable: settable_params
      ])
    end
  end

いきなり完成形の実装を持ってきてしまいましたが、このコードのどの部分が共通して利用され、どの部分がサブクラスで実装が必要な内容でしょうか?

今回、サブクラスでの実装が必要であることを明示的に示すために、枠組みを提供するメソッドでは raise 'not implemented' を定義し、サブクラスで実装が漏れた場合に例外が発生するようにしてあります。

この部分自体はテンプレートメソッドパターンとは直接的な関係はなく、後からコードを読んだ人がどの部分がサブクラスでの実装が必要なのかを判断しやすくするために定義しています。例外を使っていますが、これを良しとするかは現場によって異なりそうですね・・・。

話が少し逸れましたが、settable_classsettable_params 以外は今回実装する3つの設定で共通して使えるコードとなります。

この共通部分を基底コントローラではなく、個々のリソースに対応するコントローラでそれぞれ実装したら、それなりの分量のコードが重複することになります。仮に重複した状態で実装を進め、機能修正でアクションの内容に変更が必要になった場合、3箇所を修正することになります。

今回の例では3つのコントローラで済んでいますが、私の参加しているプロジェクトでは6個のリソースに対して実装が必要な状況でした。6個もあると、それぞれに同じ実装が書かれていると修正が漏れることも考えられます。

テンプレートメソッドパターンで基底コントローラを作成するメリットは、共通している実装を1箇所にまとめられるため、後から修正が必要になった際もその1箇所のみを変更すれば対応を完了できるという点です。

続いて、サブクラスの実装を見ていきます。

# app/controllers/notifications/frequency_settings_controller.rb
# 通知の頻度(即時、1日1回、週1回など)
class Notifications::FrequencySettingsController < Notifications::BaseController
  private

  def settable_class = NotificationSetting::Frequency

  def settable_params
    :frequency
  end
end
# app/controllers/notifications/quiet_hours_settings_controller.rb
# 通知を受け取らない時間帯
class Notifications::QuietHoursSettingsController < Notifications::BaseController
  private

  def settable_class = NotificationSetting::QuietHours

  def settable_params
    [:start_time, :end_time]
  end
end
# app/controllers/notifications/keyword_settings_controller.rb
# 特定キーワードを含む場合のみ通知
class Notifications::KeywordSettingsController < Notifications::BaseController
  private

  def settable_class = NotificationSetting::Keyword

  def settable_params
    [[
      :match_type,
      :keywords: []
    ]]
  end
end

それぞれのサブクラスは基底コントローラを継承して、settable_classsettable_params を上書きしています。 それによってクラスや受け取るパラメータの違いを表現できています。

想定するリクエストは以下のイメージです。

# POST /notifications/frequency_settings
{ notification_setting: { settable: { frequency: "daily" } } }

# POST /notifications/quiet_hours_settings
{ notification_setting: { settable: { start_time: "22:00", end_time: "08:00" } } }

# POST /notifications/keyword_settings
{
  notification_setting: {
    settable: [
      { match_type: "any", keywords: ["緊急", "重要"] },
      { match_type: "all", keywords: ["会議", "招集"] }
    ]
  }
}

以上がチャットアプリの通知設定をテンプレートメソッドパターンを活用して実装する例となります。

まとめ

テンプレートメソッドパターンを Rails の Delegated Types と組み合わせて活用し、 Rails のコントローラを DRY にすることで、異なるリソースに対して共通する実装は1箇所にまとめ、異なる部分だけを個別に定義することができました。

また、基底コントローラを継承するサブクラスで上書きが必要なメソッドを明示する方法として例外を活用するなど、後からコードを読む場合のことを考える大切さを学ぶことができました。

書くと当たり前な感じが出てしまいますが、設計は必ずこうすればいい!という正解がないため、難しいと感じるとともにそこをもっと自分なりに追求していきたいという気持ちがあります。 今回のブログではテンプレートメソッドパターンについて取り上げましたが、「他に方法はないか?」という視点も常に意識して、その時の最善を実装として落とし込めるようになれたらなぁと思っています。


株式会社永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp