Rails controllerで早期return

アプリケーションの規模が大きくなるにつれてcontrollerもfatになってくることが多いと思いますが、 リファクタする際に早期returnを使われる方も多いと思います。 controllerで早期returnをするTipsをまとめました。

Guard Clause(早期return)とは

wiki

ガード (プログラミング) - Wikipedia

によると下記のような説明がありました。

ガード (Guard) とは、コンピュータ・プログラミング言語において、条件式ないし条件分岐のような意味を持つもので、ある分岐で処理を続けるために真 (true) と評価されなければならない[1]式である。

つまり、条件分岐や関数の途中、早期に脱出する手段の一つです。ソースコードの可読性が向上し、性能もアップすることが期待されています。「ガード節」、「早期復帰」、「早期リターン」なんて呼んだりすると思います。

controllerで早期リターンするパターン

例に使ってるコード自体に意味はありません。あくまで早期returnの方法を説明することに関心を置いています。

古典的な方法

まずは古典的な方法です。

class UsersController < ApplicationController
  def show
    unless @user.logged_in?
      redirect_to sign_in_path and return
    end

    if invalid_user?
      redirect_to users_path and return
    end

    # 何らかの処理
  end
end

これは古典的な方法だと思います。 ある条件が真だった場合に、redirectさせるために return を使っています。

メソッド and 早期リターン(パターン2)

次はメソッドに切りだしたパターンです。

class UsersController < ApplicationController
  def show
    verify_user and return
    

    # 何らかの処理
  end
  
  private

  def  verify_user
    unless @user.logged_in?
      redirect_to sign_in_path and return true
    end

    if invalid_user?
      redirect_to users_path and return true
    end
  end
end

メソッドに処理を切り出した場合、例えば concerns などに切り出した処理を置いていろいろな controller で使いたい場合には注意が必要です。 このパターンはメソッド内で return true しないと新たなバグを生み出す可能性があります。 メソッドに切り出して早期リターンする場合、 true を返さないと呼び出し元の and の左辺が偽あつかいになるので処理が途中で中断されません。その場合、 DoubleRenderError が発生する場合があるので注意です。

verify_user and return という書き方が直感に反する気がしました。

メソッド or 早期リターン(パターン3)

メソッドに切り出した場合に and ではなく or を使うパターンもあります。

class UsersController < ApplicationController
  def show
    verify_user or return
    

    # 何らかの処理
  end
  
  private

  def  verify_user
    unless @user.logged_in?
      redirect_to sign_in_path and return
    end

    if invalid_user?
      redirect_to users_path and return
    end

    return true
  end
end

この場合は、メソッドに切り出したときにメソッドの最後に return true を追加する必要があります。 しかし、切り出した部分に関しては修正する必要がないので良いですね。

メソッド { return }(パターン4)

次はブロックをとるパターンです。

class UsersController < ApplicationController
  def show
    verify_user { return }
    

    # 何らかの処理
  end
  
  private

  def  verify_user
    unless @user.logged_in?
      redirect_to sign_in_path and yield
    end

    if invalid_user?
      redirect_to users_path and yield
    end
  end
end

メソッド内で return ではなく yield を使います。ブロックで return を実行している部分が yield で実行されます。 これは好みが分かれる書き方かもしれませんね。

メソッド; return if performed?(パターン5)

最後は ActionController::Metal#performed? メソッドを使用するパターンです。 このメソッドは render あるいは redirect が実行されているかどうかを判定するメソッドです。 個人的にはこの方法が1番Rails Wayっぽい感じもするけどどうなんでしょう。

class UsersController < ApplicationController
  def show
    verify_user; return if performed?
    

    # 何らかの処理
  end
  
  private

  def  verify_user
    unless @user.logged_in?
      redirect_to sign_in_path and return
    end

    if invalid_user?
      redirect_to users_path and return
    end
  end
end

まとめ

controller内で早期returnをする方法を見てきました。 特にメソッドに切り出す必要がない場合は古典的な方法を使うのが1番シンプルですね。