Slide 1

Slide 1 text

before_actionとの つらくならない付き合い方 Kaigi on Rails 2021 2021/10/23 しんくう@shinkufencer

Slide 2

Slide 2 text

本日しゃべること 2 ● 大きく2パートでしゃべります ● Part1は「before_actionで変数セットはつらいよ」 ● Part2は「before_actionがマッチする処理の見極め術」 ● Part1と2ともサンプルアプリケーションのコード使って説明します

Slide 3

Slide 3 text

今回サンプルとして扱うアプリケーション ● CGI風ゲストブック式掲示板 ● ひとこと投稿ができるアプリケーショ ン投稿一覧確認、投稿の閲覧、編集が できる ● GitHubでログインすることで ユーザー(User)が作成され 投稿(Post)ができ、編集や閲覧がお こなえる 3

Slide 4

Slide 4 text

今回サンプルとして扱うアプリケーション 4 $ bin/rails g scaffold post owner_id:bigint title homepage_url:text message:text ● 今回は「投稿」のアプリケーションなので “Post” というモデルを中心に scaffoldをつかって作ったものを基点に話を進めていきます。

Slide 5

Slide 5 text

scaffoldで作られたコード 5 class PostsController < ApplicationController before_action :set_post, only: %i[ show edit update destroy ] def index @posts = Post.all end def show end def new @post = Post.new end def edit end def create @post = Post.new(post_params) respond_to do |format| if @post.save format.html { redirect_to @post, notice: "Post was successfully created." } format.json { render :show, status: :created, location: @post } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @post.errors, status: :unprocessable_entity } end end end def update respond_to do |format| if @post.update(post_params) format.html { redirect_to @post, notice: "Post was successfully updated." } format.json { render :show, status: :ok, location: @post } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @post.errors, status: :unprocessable_entity } end end end def destroy @post.destroy respond_to do |format| format.html { redirect_to posts_url, notice: "Post was successfully destroyed." } format.json { head :no_content } end end private def set_post @post = Post.find(params[:id]) end def post_params params.require(:post).permit(:owner_id, :title, :homepage_url, :message) end end

Slide 6

Slide 6 text

scaffoldで生成されるコードについてのおさらい ● scaffoldで指定したモデル(今回はPost)を操作するためのすべてのアク ションを自動で生成します ● 単一のモデルを取り扱うアクション(showやedit)ではURLで指定したid の情報をインスタンス変数で取り扱います ○ 「パラメータを元に対象のモデルをインスタンス変数に格納する」という一連の作業を、 set_***という形で作成されます ○ set_***は各アクションで使えるようにbefore_actionで呼び出される形になっています 6

Slide 7

Slide 7 text

before_actionで 変数セットはつらいよ 7 Part 1

Slide 8

Slide 8 text

scaffoldで作られたコードにおけるbefore_actionの実行部分 8 class PostsController < ApplicationController before_action :set_post, only: %i[ show edit update destroy ] def index @posts = Post.all end def show end def new @post = Post.new end def edit end def create @post = Post.new(post_params) respond_to do |format| if @post.save format.html { redirect_to @post, notice: "Post was successfully created." } format.json { render :show, status: :created, location: @post } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @post.errors, status: :unprocessable_entity } end end end def update respond_to do |format| if @post.update(post_params) format.html { redirect_to @post, notice: "Post was successfully updated." } format.json { render :show, status: :ok, location: @post } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @post.errors, status: :unprocessable_entity } end end end def destroy @post.destroy respond_to do |format| format.html { redirect_to posts_url, notice: "Post was successfully destroyed." } format.json { head :no_content } end end private def set_post @post = Post.find(params[:id]) end def post_params params.require(:post).permit(:owner_id, :title, :homepage_url, :message) end end before_action :set_post, only: %i[ show edit update destroy ] def set_post @post = Post.find(params[:id]) end

Slide 9

Slide 9 text

before_actionとインスタンス変数 ● おさらいで話をしたとおりscaffoldではbefore_actionは各アクションで 共通に使われるインスタンス変数のセットをするためのものとして自動生 成されます ● このやりかたにならい「インスタンス変数のセットをbefore_actionで行 う」というやり方を行うコードを多々見かける場面がある ● しかしこの方法を続けるとコードが成長していくにつれてつらくなるポイ ントがいくつかある 9

Slide 10

Slide 10 text

つらくなるポイント① 依存する呼び出し 10

Slide 11

Slide 11 text

「依存する呼び出し」の例 ● 例えば「showとeditに新しく追加された投稿を最新から3件(ただし表示 している投稿以外)を出したい」というような仕様が発生したとします ● 「指定した投稿以外を取得できる」をModelに以下のように作る 11 # @param [Post] exclude_post 除外したいPost # @return [ActiveRecord::Relation] 引数指定を除いたPost一覧 def self.new_arrival_posts(exclude_post) self.where.not(id: exclude_post.id).order(created_at: :desc) end ● ViewでつかえるようにControllerでインスタンス変数にセットする必要 となる、その場合scaffoldを模して作ると次のようになる

Slide 12

Slide 12 text

Modelで取得した値をbefore_actionでsetする 12 class PostsController < ApplicationController before_action :set_post, only: %i[ show edit update destroy ] before_action :set_new_arrival_posts, only: %i[ show edit ] ################# # 中略 # ################ private def set_post @post = Post.find(params[:id]) end def set_new_arrival_posts @new_arrival_posts = Post.new_arrival_posts(@post) end def post_params params.require(:post).permit(:owner_id, :title, :homepage_url, :message) end end

Slide 13

Slide 13 text

Modelで取得した値をbefore_actionでsetする 13 class PostsController < ApplicationController before_action :set_post, only: %i[ show edit update destroy ] before_action :set_new_arrival_posts, only: %i[ show edit ] ################# # 中略 # ################ private def set_post @post = Post.find(params[:id]) end def set_new_arrival_posts @new_arrival_posts = Post.new_arrival_posts(@post) end def post_params params.require(:post).permit(:owner_id, :title, :homepage_url, :message) end end set_new_arrival_postsは@postが呼び出されて いることが前提の記述になり、依存している状態 になるので、変更に弱くなる

Slide 14

Slide 14 text

つらくなるポイント② 実行順序管理の煩雑化 14

Slide 15

Slide 15 text

「実行順序管理の煩雑化」の例 ● Scaffoldの形を意識すると、インスタンス変数の分だけbefore_actionで メソッドを書く形になってしまうため、メソッドの数が増える ● なるべく対象のアクションごとにメソッドはまとめたいが「依存する呼び 出し」がある場合に実行されるアクションの指定が同じ場合でも書き分け てあげる必要が出てきてしまう 15 ● 例えば以下のような要件が追加されると考える ● 詳細表示画面(show) に「関連投稿」「おすすめの投稿」を表示したい ○ 「関連投稿」は表示している投稿から導出できる ○ 「おすすめ投稿」は「ピックアップ投稿があれば、それを除いた新着投稿」 ● 「ピックアップ投稿」は元の投稿と新着投稿から決定される投稿 ● 「おすすめ投稿」はeditでも表示する

Slide 16

Slide 16 text

前述の仕様を実装したソースコード 16 class PostsController < ApplicationController before_action :set_post, only: %i[ show edit update destroy ] before_action :set_new_arrival_posts, only: %i[ show edit ] before_action :set_relation_posts,:set_pickup_post, only: %i[ show ] before_action :set_recommend_posts, only: %i[ show edit] ################# # 中略 # ################ # @return [ActiveRecord::Relation] 閲覧中の投稿に関連する投稿 def set_relation_posts @relation_posts = Post.relation_posts(@post) end # @return [Post] ピックアップしたい投稿、対象の投稿と新着投稿から専用のメソッドで出す def set_pickup_post @pickup_post = Post.pickup_post(current_post: @post, new_arrival_posts: @new_arrival_posts) end # @return [ActiveRecord::Relation] 状況を加味したおすすめ投稿一覧 def set_recommend_posts # おすすめはピックアップを除外した新着ポスト @recommend_posts = Post.new_arrival_posts(@pickup_post) end

Slide 17

Slide 17 text

class PostsController < ApplicationController before_action :set_post, only: %i[ show edit update destroy ] before_action :set_new_arrival_posts, only: %i[ show edit ] before_action :set_relation_posts,:set_pickup_post, only: %i[ show ] before_action :set_recommend_posts, only: %i[ show edit] ################# # 中略 # ################ # @return [ActiveRecord::Relation] 閲覧中の投稿に関連する投稿 def set_relation_posts @relation_posts = Post.relation_posts(@post) end # @return [Post] ピックアップしたい投稿、対象の投稿と新着投稿から専用のメソッドで出す def set_pickup_post @pickup_post = Post.pickup_post(current_post: @post, new_arrival_posts: @new_arrival_posts) end # @return [ActiveRecord::Relation] 状況を加味したおすすめ投稿一覧 def set_recommend_posts # おすすめはピックアップを除外した新着ポスト @recommend_posts = Post.new_arrival_posts(@pickup_post) end 順番の制約で複雑になるところ 17 最初のshowとeditのbefore_actionとして set_recommend_postsの処理も記載したかったが set_pickup_postの処理を入れる必要があるためできない

Slide 18

Slide 18 text

【Question】 before_actionで インスタンス変数のセットを つらくならないようにするには どうするのがいいの? 18

Slide 19

Slide 19 text

【Answer】 before_actionでやらない 19

Slide 20

Slide 20 text

インスタンス変数のセットはbefore_actionでやらない ● scaffoldは文字通り「足場」、足場はそのまま使うものではなく、その足 場を元に作り上げるものなので踏襲してつくらない ● アプリケーションが成長していくにつれて、各actionごとにインスタンス 変数の意味や使われ方はそれぞれ異なってくる可能性がある ● また「依存する呼び出し」のような前提として必要なインスタンス変数が ある場合、actionの中の記述で書いたほうがわかりやすい 20

Slide 21

Slide 21 text

class PostsController < ApplicationController def index @posts = Post.all end def show @post = Post.find(params[:id]) @new_arrival_posts = Post.new_arrival_posts(@post) @relation_posts = Post.relation_posts(@post) pickup_post = Post.pickup_post(current_post: @post, new_arrival_posts: @new_arrival_posts) @recommend_posts = Post.new_arrival_posts(pickup_post) end def new @post = Post.new end def edit @post = Post.find(params[:id]) @new_arrival_posts = Post.new_arrival_posts(@post) @relation_posts = Post.relation_posts(@post) @recommend_posts = Post.new_arrival_posts(nil) end ### 中略 ### def destroy post = Post.find(params[:id]) post.destroy respond_to do |format| format.html { redirect_to posts_url, notice: "Post was successfully destroyed." } format.json { head :no_content } end end private def post_params params.require(:post).permit(:owner_id, :title, :homepage_url, :message) end end before_actionをやめたコード 21 行数としては増えたが、メソッド内で変数のセット まで完結しているので見やすい また、pickup_postはインスタンス変数である必要 がないのでスコープを狭くすることができる destroyのactionはインスタンス変数を必要としな いのでローカル変数に書き換えることができた

Slide 22

Slide 22 text

インスタンス変数セットが ダメなのはわかった ではbefore_actionは どのような場面で使うとよいのか? 22

Slide 23

Slide 23 text

before_actionが マッチする処理の見極め術 23 Part 2

Slide 24

Slide 24 text

「before系」フィルタのよくある使われ方として、ユーザーがアクションを実行する前にログイ ンを要求するというのがあります。このフィルタメソッドは以下のような感じになるでしょう。 before_actionはどういう処理とマッチするのか ● before_actionが合う処理の具体例はRailsガイドにすでに書いてある 24 “ ” class ApplicationController < ActionController::Base before_action :require_login private def require_login unless logged_in? flash[:error] = "You must be logged in to access this section" redirect_to new_login_url # halts request cycle end end end Action Controller の概要 - Railsガイド より https://railsguides.jp/action_controller_overview.html

Slide 25

Slide 25 text

before_actionはどういう処理とマッチするのか ● Railsガイドの例をもう少し拡張し、「新規作成ページと編集ページはログ インしていないと見れない」とすると以下のような次のようなコードにな る 25

Slide 26

Slide 26 text

新規作成ページと編集ページはログインしていないと見れない 26 class ApplicationController < ActionController::Base helper_method :logged_in? helper_method :session_user private def session_user @memorize_session_user ||= User.find_by_id(session[:user_id]) end def logged_in? !!session_user end def require_login return if logged_in? redirect_to root_path, notice: "ログインしてください " end end class PostsController < ApplicationController before_action :require_login, only: %i[ new, edit] こちらのコードは「パーフェクトRuby on Rails【増補改訂版】」を元に作成しました https://gihyo.jp/book/2020/978-4-297-11462-6

Slide 27

Slide 27 text

before_actionを使う使わないの見極め ● Railsガイドでもあるように「ログイン状態の事前確認」には適している ● 「複数の場所で使うようインスタンス変数のセット」には適していない 27 事例はわかったが「どういう処理で使って良いのか」の判断が分類しにくい そのため今回見極めのひとつの方法として「横断的関心事」を取り上げます

Slide 28

Slide 28 text

28 横断的関心事 Crosscutting-Concern

Slide 29

Slide 29 text

横断的関心事とは ● 大きなプログラムの中で、それぞれの処理において「その処理が関心のあ ることがら(内容)」に関して示す言葉として関心事と表現する。 ● メソッドなどを実装するときにそのメソッドが主要として考えるべき内容 のことを本質的関心事(core concern) , それに対して多くの部分で使われ るような共通的な処理のことを横断的関心事(cross-cutting concern)と いう 29 関心事というフレーズにしては以下のページにまとめてます 「関心の分離」をするメリットを料理レシピを通して考える - コード日進月歩 https://shinkufencer.hateblo.jp/entry/2021/09/18/233033

Slide 30

Slide 30 text

特殊計算処理メソッド 横断的関心事とは 30 処理開始時間Logging 計算処理 処理終了時間Logging 横断的関心事の例としてLoggingがよくあげられる 「計算処理」から見たときにLogging処理自体は 本当に行いたい処理かといわれるとそうではなく また、様々なところで共通して利用できるので Logging処理は”横断的な関心”となる。 こちらはBEAR.Sundayの説明スライドを参考にさせていただきました Slide | BEAR.Sunday https://bearsunday.github.io/slide.html

Slide 31

Slide 31 text

今回の例を踏まえた横断的関心事 例えば「投稿の詳細表示ページ」で処理させたいことは… 31 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること 選択した投稿をふまえ たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト

Slide 32

Slide 32 text

横断的関心事 本質的関心事 今回の例を踏まえた横断的関心事 32 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること 選択した投稿をふまえ たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト 編集ページでは「投稿の閲覧」にまつわることがメインなのでそれらを本質的 な関心として捉えると以下のように分けることができる

Slide 33

Slide 33 text

横断的関心事 本質的関心事 今回の例を踏まえた横断的関心事 33 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること 選択した投稿をふまえ たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト ログインのチェック自体は編集する際には必須の処理であるがアプリケーショ ン全体で横断して関心のある処理なので横断的な関心として捉える

Slide 34

Slide 34 text

横断的関心事 本質的関心事 今回の例を踏まえた横断的関心事 34 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること 選択した投稿をふまえ たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト 関連投稿に関しては投稿詳細(show)でも使える共通処理ではあるが、他の場所 に使うような横断的なものか?と考えると趣が異なるため、別と考える

Slide 35

Slide 35 text

横断的関心事に着目するということ ● このように「その処理で横断的な関心事は何か?」という目線を持ち込む ことでbefore_actionに適当な処理を絞り込むことができる ● 複数の処理で使う似たような処理が出てくるケースがあるがそれはDRY原 則として「共通化できる処理」である場合が多く、横断的であるかはまた 別になるのでそのような処理の見極めにもこの切り口は役に立つと思われ る 35

Slide 36

Slide 36 text

まとめ 36 ● インスタンス変数をsetするbefore_actionは複雑化するのでやめよう ● before_actionを利用すべきかどうかは判断しづらいことも多いので 「横断的関心事」かどうか?で見極めると分けやすいのでオススメ

Slide 37

Slide 37 text

37 Thanks! 参考にしたサイトなど: しんくう / shinkufencer   @shinkufencer コード日進月歩 https://shinkufencer.hateblo.jp/ Rails の before_action :set_* って必要? - ネットの海の片隅で https://osa.hatenablog.com/entry/good-bye-before-action-setter Controllerのbefore_actionにおける インスタンス変数セットについて https://www.slideshare.net/pospome/controllerbeforeaction