Rails: 提案「コントローラから`@`ハックを消し去ろう」(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Removing the @ Hack in Rails Controllers – Eric Anderson – Medium 原文公開日: 2018/06/02 著者: Eric Anderson 週刊Railsウォッチで絶賛された記事です。Rails Wayから外れるのでRails中級以上向けですが、Rails初心者も知っておいて決して損はありません。 太字は訳で追加いたしました。 Rails: コントローラから@ハックを消し去る(翻訳) 少し前、とある記事を書いたとき、Railsコントローラをメンテしやすくするために私が用いていた、伝統的でない戦略をいくつかご紹介しました。今でも考え方そのものにはまったく問題はないと思っていますが、その中でもとりわけ気に入っているものについては、Railsで標準になるべきだとも思っています。 そのために本記事は、Railsのコントローラでデータの読み込みやアクション間での共有、ビューとのやりとりの手法を変更すべきであるという提案を皆さんに納得していただくための事例を作成するために執筆しています。 「Rails Way」のおさらい 私の事例をご紹介する前に、「Rails Way」のどの点が素晴らしく、どの点が不十分であるかをしっかり理解しておくことが重要と考えます。それによって、私からの提案がさらに明瞭になればと願っています。 データの読み込みやビューとのデータのやり取りを、冗長な(Rails的でない)方法で行うと次のようになります。 def show user = User.find params[:id] render :show, locals: { user: user } end 運のよいことに、Railsフレームワークの開発者はどのアクションも最終行が似たり寄ったりであることに気付いたので、テンプレートに渡す必要のあるデータを何度も何度も書くことにきっと嫌気がさしたのでしょう。そしてこうした定型コード(boilerplate)を減らすために賢い方法を2つ編み出しました。 @変数のハック 暗黙のレンダリング では、これら2つのRails的手法を適用するとどうなるかを見てみましょう。 def show @user = User.find params[:id] end 暗黙のレンダリングでは、レンダリングするテンプレートをアクション名で決定します。私はこちらのRails Wayを愛していますので、これは変えたくありません。問題にしたいのは、もうひとつの@変数のハックの方です。 @変数のハック そもそも「@変数のハック」とは何でしょうか。Railsフレームワークの開発者は、何らかのハッシュ的なものを用いてビューに変数を渡すのではなく、その変数にマーキングするだけの方がよいだろうという決定を下したのです。 マーキングはどのようにして行われるかご存知でしょうか?Ruby自身には、変数にマーキングする方法が2とおりあります。変数の先頭に$記号または@記号を付けることでマーキングできます。 Rubyで$記号を付けると、値がグローバルになるのでよくありません。では@記号はどうかというと、通常であれば、その変数は同一のインスタンス上でのあらゆるメソッド呼び出しで共有されるべきであるという指定になります。これはRubyや多くのオブジェクト指向言語でネイティブでサポートされている「インスタンス変数」と呼ばれるものです。 Railsフレームワークの開発者は、コントローラではインスタンス変数の使いみちがあまりないことに気付きました。コントローラは、概念上(開発者目線では、ですが)以下を行います。 controller = UsersController.new request response = controller.show もちろん内部では他にもいろいろやっているのは承知のうえで、概念上は以下にまとめられます。 インスタンスを1つ作成する メソッドを1つ呼び出す 作成されたオブジェクトを渡す これは本質的にオブジェクト指向的というより関数型的です。 コントローラオブジェクトはさほど長生きしませんし、呼び出すメソッドはたった1つなので、コントローラではインスタンス変数の出番があまりありません。Railsフレームワークの開発者はここに目をつけて、@というマーカーを別の目的に転用する決断を下したのです。 技術的にはインスタンス変数であることは変わりませんが、Railsはこれらの変数を監視してビューにコピーします。これによって、ビューに渡す変数のハッシュを指定する必要がなくなりました。 ある機能を(言語設計者の)意図しない目的に転用するという意味で、私はこれをハックと呼んでいます。「ハックだからよくない」ということではありませんのでお間違いなきよう。定形コードを削減するために未使用の機能を転用するのは賢いやり方です。私が問題にしたいのは、このハックそのものではなく、実際にはこのハックでも満たせないニーズがあるという点に尽きます。 何が問題なのか @変数のハックのどのあたりに問題があるのでしょうか?@マーカーを転用したことが問題なのではなく、読み込みのパターンが複数のアクション間で共有されてしまっていることが問題なのです。次のように、いくつものアクションが同じようなデータを欲しがるというのはよくあることです。 def show @user = User.find params[:id] end def edit @user = User.find params[:id] end 他のupdateやdestroyなどのメンバーアクションも同様です。Railsには、甚だしい繰り返しをコールバックで解決する手法があります。上のように書く代わりに、以下のように書くことができます。 before_action(only: %i[show edit]) { @user = User.find params[:id] } def show end def edit end しかしコールバックによる手法にはいくつもの問題があります。 only:やexcept:を用いて特定のアクションだけを対象にしようとするとエラーが発生しやすくなります。これらのリストがちゃんとメンテされていないばかりに誤ったデータを読み込んでいたアプリを山ほど目撃してきました。 アクションの実際の動作が見えづらくなります。アクションだけを眺めても、何もしてないように見えてしまいます。 現実世界の巨大なコントローラでコールバックを把握するのはつらい作業になる可能性があります。コールバックが親クラスで定義されていたりモジュールとしてincludeされていればなおさらです。 ソースコードの順序がシビアになります(あるbefore_actionフックで別のbefore_actionで読み込んだデータが必要になるなど) 「メソッドでやろうよ」 私の提案するソリューションはいたってシンプルです。一言で言えば「メソッドでやろうよ」です。 私の提案では私たちのニーズがすべて勘案されているので、まともな定形コードを素早く構築できます。定形コードはまさに@ハックが殺そうとしていたものなのですから、ぱっと見歴史を繰り返しているように思われるかもしれません。私のアイデアがお気に召すかどうかを皆さんにご検討いただくためにも、どうかもうしばらくお付き合いください。最終的には、ささやかなメタプログラミングを用いてあらゆる定形コードをラップし、(@ハックの)メリットを失うことなく簡潔なコードに作り変えます。 メソッドを定義する 典型的なオブジェクト指向システムにおいて、あるコンポーネントが他のコンポーネントからデータを取得する最もシンプルなソリューションと言えば何だかおわかりでしょうか?最初のコンポーネントが、その情報を担当する次のコンポーネントにメッセージを送信することです。この「メソッドを介するメッセージ送信」は、オブジェクト指向プログラミングの核となるアイデアです。 さて、データをビューに送信しようとするのではなく、両者の立場を逆転させて、ビューが自分の欲しいデータをコントローラに問い合わせる形にしてはどうでしょうか?つまり、データを要求するメッセージはビューからコントローラに送信し、コントローラはレスポンスでデータを返すことになります。 ビューでは以下のような感じになります。 <%= controller.user.name %> そしてコントローラは次のような感じになります。 def user User.find params[:id] end これは単なるpublicメソッドであり、オブジェクト指向としてはごく普通の考え方です。このメソッドは別のテンプレートからも使えます。editテンプレートとshowテンプレートのどちらもuserを欲しがっているのであれば、どちらも同じメソッドを呼べばよいのです。indexテンプレートでは欲しくないのであれば、indexテンプレートで呼ばなければよいのです。必要もないのにデータを読み込むことはしません。コールバックの定義でonly:やexcept:のリストを今後いつまでもいじくりまわす必要もありません。 メモ化 userの属性を大量に出力したいが、コントローラで新しいインスタンスを毎回読み込むのは嫌だ。そんなときは次のように読み込みをメモ化(memoize)しましょう。 def user @user ||= User.find params[:id] end 上のコードではあえてインスタンス変数を用いていることにご注意ください。しかしこれはインスタンス変数の本来の用い方なのです(同一インスタンス内にある異なるメソッド呼び出し同士でデータを共有する)。上のコードではインスタンス変数がnilの場合が考慮されていませんので、もう少しちゃんと書くと次のようになります。 def user return @user if defined? @user @user = User.find params[:id] end ヘルパー もはや概念上は「ハック」ではなくなりましたが、その分テンプレートはわずかに冗長になりました(コントローラも冗長になりましたが、これについては後述します)。いちいちcontroler.なんちゃらみたいな変数にアクセスしたくはありません。コントローラへのプロキシを次のようなヘルパーメソッドにしてみてはどうでしょうか。 module UsersHelper def user controller.user end end これでテンプレートは以下のように書くだけで済みます。 <%= user.name %> これはこれでありがたいのですが、データ読み込み系メソッドごとにヘルパーメソッドをいちいち書くのはだるくて仕方ありません。幸いなことに、Railsにはこんなときに使える手があります。コントローラのどのメソッドでも、helper_methodを呼んでおきさえすればRailsがヘルパーメソッドを代わりにこしらえてくれます。これでコントローラのデータ読み込み部分は次のように書けます。 def user return @user if defined? @user @user = User.find params[:id] end … Continue reading Rails: 提案「コントローラから`@`ハックを消し去ろう」(翻訳)