10
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者】同じコードを繰り返さないためのTips(Railsコントローラー編)

Last updated at Posted at 2025-12-11

はじめに

RUNTEQ Advent Calendar 2025 の12日目を担当します、MOと申します。
私は、未経験からのエンジニア転職を目指してプログラミングスクールRUNTEQで Ruby on Rails を中心とした学習をしています。

今回は、卒業制作を通して学んだ「同じコードを繰り返さないためのTips(コントローラー編)」を忘備録も兼ねてまとめてみます。
未熟な点も多いですが、少しでも私のようなRails初心者さんの参考になると嬉しいです。

初心者による執筆につき、内容に不備や誤りがある可能性がございます。
また 本記事で伝えたいことに記載の通り、現在進行形で理解を進めている内容です。
もし間違いや改善点があれば、コメント等でご教示いただけると幸いです。

本記事で伝えたいこと

  • コントローラーで同じコードを繰り返さないためのMyTips3点。
    1. 同一コントローラー内の共通処理をまとめる(privateメソッド・before_actionなど)
    2. 複数コントローラー間の共通処理をまとめる (concern)
    3. よく使う条件句をscopeでまとめる
  • 特に、【concern】 を使うとコントローラーがすっきりします。
  • 本記事で紹介するTipsが常に正しいとは限らないようです。今後はアンチパターンやService Objectの理解も深めたいと思います。

筆者環境

Ruby 3.3.6
Ruby on Rails 7.2.3

共通処理をまとめる

① 同一コントローラー内 : privateメソッド や before_action でまとめる

クリスマスが近づいてくると、「同じコントローラー内の複数アクションで同じメソッドを使いまわしたい」なんてことがありますよね。

例えばこのgame_records_controller.rbさん。
@question = Question.includes(origin_words: :related_words).find(params[:id])が何度も登場してますね。
そのうちincludesの中身が増えたりしたら何度も書き換えないといけなくてめんどくさいですね。
これ、メソッドとかにまとめたいですね。

app/controllers/game_records_controller.rb
class GameRecordsController < ApplicationController
    
    def create
        @question = Question.includes(origin_words: :related_words).find(params[:id])
        ...省略...
    end
    
    def show
        @question = Question.includes(origin_words: :related_words).find(params[:id])
        ...省略...
    end
end

set_question メソッドに纏めてしまいましょう。
ついでに、create アクションと show アクション実行前にbefore_actionで set_question メソッドを呼び出してやりましょう。
これで突然 includes の中身が変わっても1か所の書き換えで対応完了できます。

app/controllers/game_records_controller.rb
class GameRecordsController < ApplicationController
    # 【追記する】↓ before_action で、set_question を呼び出す
    before_action :set_question, only: %i[create show]
    
    def create
        #【削除する】set_question と同じ働きのコードは削除する
        ...省略...
    end
    
    def show
        #【削除する】set_question と同じ働きのコードは削除する
        ...省略...
    end

    # 【↓ 追記ここから ↓】
    private

    def set_question
        @question = Question.includes(origin_words: :related_words).find(params[:id])
    end
    # 【↑ 追記ここまで ↑】
end

② 複数コントローラー間 : concernでまとめる

この人生、やっぱり 「複数のコントローラーで共通のset_questionを使いたい瞬間」 ってのもあるんですね。
そういう時、全部のコントローラーに毎回set_questionを定義するのではなく、
concernで定義したものを各コントローラーで呼び出すようにするとset_questionの定義を変更するのが楽になります。consernだけ修正すればおしまいなので。

というわけで、例を見てみましょう。
例では、私がRUNTEQ卒業制作として開発したNeuroWordというアプリのソースコードを使います。
このアプリでは、様々な場面で以下のようにset_questionを定義しています。

def set_question
    @question = Question.includes(origin_words: :related_words).find(params[:id])
    # ↑長々書いてますが、「生麦生米生卵」くらいの意味に捉えておいてください。
end

@question = Question.includes(origin_words: :related_words).find(params[:id])については、生麦生米生卵くらいの意味だと捉えていただければOKです。
これは、**「生麦生米生卵茹で麦茹で米茹で卵に手っ取り早く書き換えたいという物語」**です。

前置きが長くなりました。
今度こそ複数コントローラーの共通処理をconcernsでまとめた例を見ていきましょう。

😴concern使用前

3つのコントローラーを例示します。
「全部に@question = Question.includes(origin_words: :related_words).find(params[:id])が出てくるんだな~」と分かればOKです。
つまり、「生麦生米生卵茹で麦茹で米茹で卵に書き換えるには、3か所以上修正が要るんだな~」ということです。めんどくちゃ…げふんげふん。

app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
    def show
        @question = Question.includes(origin_words: :related_words).find(params[:id])
        # ↑注目!
        ...省略...
    end
end
app/controllers/games_controller.rb
class GamesController < ApplicationController
    before_action :set_question, only: %i[show check_match]
    
    def show
        ...省略...
    end
    
    def check_match
        ...省略...
    end

    private

    def set_question
        @question = Question.includes(origin_words: :related_words).find(params[:id])
        # ↑なんか見覚えありますね!
    end
end
app/controllers/game_records_controller.rb
class GameRecordsController < ApplicationController
    before_action :set_question, only: %i[create show]
    
    def create
        ...省略...
    end
    
    def show
        ...省略...
    end

    private

    def set_question
        @question = Question.includes(origin_words: :related_words).find(params[:id])
        # ↑またしても現れる生麦生米生卵 in set_question!
    end
end

🥳concern使用後

まず、app/controllers/concerns/find_question.rbという新しいファイルからご覧ください。

app/controllers/concerns/find_question.rb
module FindQuestion
  extend ActiveSupport::Concern
 # ↑ FindQuestion は、ファイル名を🐫キャメルケース🐪で表記したものですね。
  # ↑ FindQuestion 以外は定型として使いましょう~!
 
  private
 # ↑ 今回は、このファイルを呼び出すコントローラー外で ↓のset_questionを使うことはないので、
 # private内で set_question を定義しています。

  def set_question
    @question = Question.includes(origin_words: :related_words).find(params[:id])
    # ↑ set_question の中で、あの生麦生米生卵を定義してますね👀
  end
end

そして、先ほどと同じ3つのコントローラーを確認してみましょう。

app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
    include FindQuestion
    # ↑ここで、concerns/find_question.rb を呼び出す呪文を詠唱!

    def show
        set_question
        # ↑ concerns/find_question.rb で定義した set_question が使えるようになりました🙌
        ...省略...
    end
end
app/controllers/games_controller.rb
class GamesController < ApplicationController
    include FindQuestion
    # ↑ここで、先ほどと同じ呪文を詠唱!
    
    before_action :set_question, only: %i[show check_match]
    # ↑ concerns/find_question.rb で定義した set_question が使えるようになりました🙌
    # このファイル内の private下で set_question を定義する必要はありません🙌
    
    def show
        ...省略...
    end
    
    def check_match
        ...省略...
    end
    
end
app/controllers/game_records_controller.rb
class GameRecordsController < ApplicationController
    include FindQuestion
    # ↑お馴染みの呪文を詠唱!
    
    before_action :set_question, only: %i[create show]
    # ↑ concerns/find_question.rb で定義した set_question が使えるようになりました🙌
    # このファイル内の private下で set_question を定義する必要はありません🙌
    
    def create
        ...省略...
    end
    
    def show
        ...省略...
    end
    
end

… お気づきいただけましたでしょうか?

@question = Question.includes(origin_words: :related_words).find(params[:id])つまり生麦生米生卵が、1度しか、concerns/find_question.rb でしか登場していないということに。
concern未使用の場合は3つのコントローラー全てに登場していたあの生麦生米生卵が1度しか登場していないのです。
つまり、concernに共通処理をまとめた世界で生麦生米生卵茹で麦茹で米茹で卵にしたくなったら、concernのファイルだけ変更すれば良いのです。

【まとめ】concernsに共通処理をまとめる方法

  1. 複数のコントローラーで共通の処理になっている部分を発見する
  2. 1で発見した共通処理を定義するためのファイルをapp/controllers/concerns配下に作成する。
    ファイル名はスネークケース(単語同士の文字区切りで_を使う書き方).rbとする。
  3. 2で作成したファイルに、以下の通り記述する
    ※example_concernやExampleConcernは、適宜変更してください。(スネークケース・キャメルケースに注意!)
    app/controllers/concerns/example_concern.rb
    module ExampleConcern
      extend ActiveSupport::Concern
    
      ...1で発見した共通処理を記述...
    end
    
  4. 共通処理を使用するコントローラーを、以下の通り変更する
    app/controllers/xxxx_controller.rb
    class XxxxController < ApplicationController
      # 【追記】↓concernの呼び出しを追記する
      include ExampleConcern
      
      # 【削除】concernsにまとめた共通処理の記述を削除する
    end
    

よく使う条件句を scopeでまとめる

concernはapp/controller配下で設定していたのに対し、scopeはモデル内で設定します。
また、concernはリクエストの処理ロジック共通化に適しているのに対し、scopeはデータの取得条件共通化などに適しているようです。

こちらも、先述のNeuroWordというアプリのソースコードを例として見てみます。

🤔scope使用前

questions_controller.rbを見てみます。
2か所で.joins(:tags).where(tags: { name: params[:tag_name] }) が使われています。

app/controllers/questions_controller.rb (抜粋)
def index
    if params[:tag_name].present?
      @selected_tag = Tag.find_by(name: params[:tag_name])
      base_query = base_query.joins(:tags).where(tags: { name: params[:tag_name] }) if @selected_tag
      # ↑ .joins(:tags).where(tags: { name: params[:tag_name] }) に注目!
    end
end

# カテゴリごとの絞り込み後件数を計算
def calculate_category_counts_with_filters
    # タグ・検索条件のみ適用(カテゴリは除外)
    filter_query = Question.all
    
    if params[:tag_name].present? && @selected_tag
      filter_query = filter_query.joins(:tags).where(tags: { name: params[:tag_name] })
      # ↑ .joins(:tags).where(tags: { name: params[:tag_name] }) に注目!
    end
end

🧐scope使用後

まず、scopeを設定しているmodels/question.rbを見てみましょう。
scope :tagged_with は、tagged_with という名前のスコープを定義します。
->(tag_name) はラムダ(匿名関数)で、引数として tag_name を受け取ります。このスコープが呼ばれるたびに、引数として渡されたタグ名に基づいて検索条件を生成します。

app/models/question.rb (抜粋)
scope :tagged_with, ->(tag_name) { joins(:tags).where(tags: { name: tag_name }) }

続いて、もう一度questions_controller.rbを見てみます。
先ほどtagged_with という名前のスコープを定義したことにより、.joins(:tags).where(tags: { name: params[:tag_name] }) だった部分が tagged_with(params[:tag_name]) に変わり、少しすっきりしました。

app/controllers/questions_controller.rb (抜粋)
def index
    if params[:tag_name].present?
      @selected_tag = Tag.find_by(name: params[:tag_name])
      base_query = base_query.tagged_with(params[:tag_name]) if @selected_tag
      # ↑ .joins(:tags).where(tags: { name: params[:tag_name] }) が tagged_with(params[:tag_name])に!
    end
end

# カテゴリごとの絞り込み後件数を計算
def calculate_category_counts_with_filters
    # タグ・検索条件のみ適用(カテゴリは除外)
    filter_query = Question.all
    
    if params[:tag_name].present? && @selected_tag
      filter_query = filter_query.tagged_with(params[:tag_name])
      # ↑ こちらも tagged_with(params[:tag_name])に!
    end
end

おわりに

記事執筆にあたり過去記事などを調べる中で、concernやscopeも使い方によってはアンチパターンに該当することを知りました。
アンチパターンについては私の理解度が不十分なので本記事での説明は控えます。
気になる方はぜひ参考記事や「機能名 + アンチパターン」の検索結果もご覧ください。

🎄 🎅 🎄 🎅 🎄 🎅 🎄 🎅 🎄 🎅 🎄 🎅 🎄

今回紹介できなかった共通化手法として、Service Project なる方法もあるそうです。
いずれはこちらも使って、他の共通化手法との違いを体感したいと思っています。

🎄 🎁 🎄 🎁 🎄 🎁 🎄 🎁 🎄 🎁 🎄 🎁 🎄

ということで(?)
私のようにほぼ初めてのアプリ作成であれば、まずは機能を使ってみて、メリットデメリットを体感することも大切かと思います。
ごちゃっとしていたコードがすっきりさっぱりしていく様を体感するのは、なかなか気持ち良いものです。
この気持ちよさを技術記事で共有しようとしたら、新たな疑問の扉が開かれたり。

この奥深い開発の世界、これからもぜひヒーヒーウンウン言いながら一緒に楽しんでいきましょう☺️

🎄 🎅 🎄 🎅 🎄 🎅 🎄 🎅 🎄 🎅 🎄 🎅 🎄

拙い記事を最後まで読んでいただき、ありがとうございました!
良い年末&開発ライフを!!
明日のRUNTEQ Advent Calendar 2025もお楽しみに!🎁✨️

🔗参考URL

10
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?