3年以上かけて培ったRails開発のコツ集大成(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社エンジニアの「?」を「!」に。原著者の許諾を得て翻訳・公開いたします。Vasilakis3年以上かけて培ったRails開発のコツ集大成(翻訳)順序は特に決まっていません。トップレベルにrescue_fromを書く(permalink)ルートのコントローラにrescue_fromを書くと、その下で発生したすべての例外をキャッチできるので非常に便利です。Webアプリにこれを追加すると、リクエスト/レスポンスのサイクルで実行されるほとんどのコードが一気に便利になります。シンプルなAPIを例に考えます。rescue_fromを使えば、レコードが見つからない(ActiveRecordがActiveRecord::RecordNotFoundをスローする)場合のアプリの振る舞いを明示的に指定できます。コントローラにload_resourceを書く(permalink)もうひとつのパターンは、以前同僚が使っていたのを見て以来採用しているものです。必要なリソースのフェッチをコントローラのメソッド内で行う代わりに、共通のコントローラのフィルタを使い、アクションの実行に応じてフェッチするというものです。endendこれの発展版がdecent_exposureです。私自身はまだ使う機会がありませんが。コメントを避けてコードに語らせる)という言説にあまり賛成できません。ある開発者にとってよいコードであっても、別の開発者にとっては悪いコードになることもあります(スタイルは人それぞれなので、それ自体が悪いのではありません)。時間や予算の制約から、手早く修正してissueをクローズするしかないという状況はいくらでもありえます。最善の(そして最も自明な)ソリューションは「10倍努力する」ということがわかっていてもですになるかもしれないためです。いうわけで、「コードが匂ってるな」と思ったら、恥ずかしがらずにどしどしコメントしましょう?。02はその後議論になっています。みたいにインスタンス変数を暗黙的にセットしたりするの、ほんと嫌いなのでほんとやめてほしい。decoratorやpresenterを使う(permalink)しばらく前から、「モデルをファットにして、その分コントローラを薄くせよ」という言説をRailsコミュニティで見かけます。「コントローラを薄くせよ」については同意しますが、ファットモデルについては同意できません?。モデルもできるだけ薄くするべきであり、特殊の場合にしか使わないようなプロパティをモデルで自動生成しないことです。そのような場合はラッパークラスを使って(皆さん、これがdecoratorですよ!)必要なメソッドだけを公開しましょう。presenterはdecoratorと似ていますが、複数のモデルを扱う点だけが異なります。モデル配下のワーカーを名前空間化してafter_commitで呼び出す(permalink)Userというモデルがあるとしましょう。あるモデルに関連するバックグラウンドジョブの90%は、モデルの作成/更新/削除で発生します。ここでデータが変更されるからです。ここから、User::CreateWorker、User::UpdateWorker、User::DestroyWorkerという3つの一般的なワーカーを導き出せます。利用可能な場合にはこれらのワーカーをActiveRecordコールバックやprevious_changesと組み合わせて使ってみましょう。ワーカーの呼び出しはafter_commitで行います。理由についてはこちらをご覧ください。PostgreSQLのarrayは、よほどシンプルでない限り使わないこと(permalink)PostgreSQLのarrayもクールなのですが、私の経験では、時間を節約するより問題を生み出す方が多くなります。PostgreSQLのarrayを使うと(何らかのIDを保存するなど)、後でそのテーブルを見たときに必ず私の頭が爆発しました。データベースにはJOINというものがあるのですから、テーブルを追加するコストは高くありません。PostgreSQLのarrayは、うんと小規模な場合にしか使わないことにします。JSONBはいいヤツ(permalink)PostgreSQLのarrayとは対照的に、PostgreSQLのJSONBは大好きです。データベースにスキーマがあることのメリットは明らかなので、私たちは皆スキーマを持つデータベースが大好きです。スキーマを事前に予測できない場合、スキーマレスデータベースのシンプルさがどうしても必要になることがあります。私は次のような場合にJSONBを使うことがよくあります。小さな属性を多数使うことがあり、しかも親属性で名前空間される可能性もある場合。普通のテーブルでこれをやると、カラムだらけになってしまいます。hydrationは化学用語の「水和/水和物」の借用で、シリアライズに少し似た概念です。ステートマシンで状態や操作を強制することができ、専用のきわめてシンプルなDSLを使えます。オブジェクトを初期状態と異なるステートで作成するとフックが動作しないという問題が生じます。aasmの内部状態とにらめっこして頑張るか、あきらめてオブジェクトの特定のステートを手動でスキャンすることになります(それ用のサービスを作ったりとか)。.メールアドレスのバリデーションを頑張るよりgemを使う(permalink)メールアドレスのバリデーションに使う正規表現をググると毎回違う正規表現が見つかるのは、もう笑うしかありません。完璧な正規表現を探すのはとっととあきらめて、おとなしくgemを使うに限ります。decoratorやpresenterを使って、ビューに渡すインスタンス変数をなるべく1つだけにする(permalink)私がRailsで残念に思っている部分です。コントローラからビューにコンテキストを渡すのにインスタンス変数をいくつも使うのは、バッドプラクティスだと思います。Metzの言うとおり、インスタンス化して渡すオブジェクトは常に1つだけにすべきです。モデルに保存するインスタンスメソッドに!を付ける(permalink)モデルのメソッドがオブジェクトを変更してデータベースに保存する場合、メソッド名の末尾に必ず!を付けてそのことを示しましょう。クラスレベルで厳密なAPIを書くことがコードの品質を高めることにつながりますが、開発者はそのことを忘れがちです(私もですね!)。gemを使わないこと(permalink)Deviseはマジックが多すぎます。gemを多用していましたし、今も使っています。Object)でモデルのように振る舞うオブジェクトを構成でき、属性をある程度厳密に保つこともできます。私は、属性が増えすぎたときに次のようなVirtus向けの独自DSLを書いて属性を操作できるようにすることがよくあります。VirtusModelendさまざまな属性の種類を列挙することも、属性のグループにある種のタグを追加することもできます。おかげで私はニッコニコです?。APIができたので、これで同じか似たようなことができるのではないかと考えています?。外部API参照などの重たい処理にはメモ化(memoization)を使う(permalink)もうおわかりですよね?。PostgreSQL全文検索は単純な用途に向いている(permalink)pg_searchは驚くほど簡単にセットアップできます。tvectorsなどでPostgreSQL全文検索を最適化しなければならない場合は、素直にElasticSearchを使いましょう。PostgreSQLでそれ以上時間をかけるのは無駄です。Objectのもっと明確な定義と、どのように実装すべきかを今も探し続けています。私たちが最近手がけた案件では、あるパターンに従うことで再利用が楽になりました。最初に、モジュールを1つ作成します。これをincludeすると、performという名前のクラスメソッドを作成します。次に、作成するすべてのサービスで、コンストラクタ(initialize)をprivateにします。このperformパブリッククラスメソッドだけを呼ぶということです(もちろんRubyのような動的言語ではprivateメソッドも呼ぼうと思えば呼べますが、単に呼びにくくするだけの処置です)。ActiveRecordのエラーメッセージを好みの形に変換する(permalink)RailsでAPIを書くと、エラーメッセージはたいていJSONAPI形式に従います。blank)とメッセージが失敗した属性(user_id)が出力されます。この例ではJSONポインタを使っていませんが、これにも同じアイデアを適用できます。クライアント側では好みに応じて次の2つの方法でこれらを扱います。blank」などのように読みやすい形に変換するかです。メッセージに関連する属性がユーザーにとって意味のないものである場合はどうなるでしょうか。このアプリで、各ユーザーは新しい投稿(post)を1つ作成できるとします。投稿は1日1回までだとします。モデルで次のようにして一意性を強制します。}}(はい、DBレベルでも同じように一意性制約をかけるべきですよね、わかっております。ここでは仮に、ユーザーが2つの異なるサーバー(しかも同じアプリが動き、同じDBを使っている)にアクセスして、運よく(運悪く)2つのリクエストを完全に同時に受け取れないと困るので、このエラーは扱いません)このときのメッセージは次のようになります。]}ユーザーはこれを渡されても困ってしまいます。1つの方法は、messageオプションを使うことです。'さんの投稿は1日1回までです']のように多少読みやすくなりましたが、両方の属性を使う場合にあまり便利ではありません。]}理想は、このメッセージをbaseに移動することです。このメッセージは特定のモデル属性に依存しない、より一般的なカスタム制約だからです。これは、メッセージにカスタムDSLを追加すればできるようになります。errors_arrayendこれで、使いたい属性に合うエラーが出力されます。値を返すメソッドでは明示的にreturnを書く(ワンライナーであっても)(permalink)Rubyコミュニティはreturn文を書かないことにこだわっていると思いますが、私はそこにこだわる理由はない気がしています。実際私は、たとえワンライナーであっても、副作用が目的ではなく戻り値を目的とすべき場合はreturn文を追加しています。Rubyのクールさと表現力を云々することよりも、生産性と(ある種の)安全性の方が勝ります。かっこ()を使う(ある種のDSLを使う場合を除く)(permalink)これも同様です。かっこを追加して困ることはありませんし、普段他の言語も使っている同僚が幸せになれます。env変数に厳密な論理値型を追加する(permalink)私はconfig/sercrets.ymlで次のようなスニペットを使うのが好きです。true}%>こうすることで、論理値型のenv変数がtrueかfalseのどちらかだけを取るようになるので、コードで使いやすくなります。PostgreSQL以外のデータベースをメインで使うのであれば十分な理由付けが必要(permalink)MongoDBはひと頃もてはやされていましたが、ほどなくしてMongoDBの欠点が知られるようになりました。スキーマレスは機能の1つだと思うかもしれませんが、実際には大きな欠点です。データベースにスキーマがあることで、スキーマを必要に応じて少しずつ変更できますし、ツールや保証も得られます。SQLにinteger型のカラムが1つあるとすると、これをstring型やtext型に変更することも、デフォルト値の設定やNULL禁止の設定もできます。これはスキーマレスなデータベースでは不可能であり、プログラミング言語を用いて高度なレベルで自作する必要があります。スキーマレスなデータベースでは、属性の追加や削除も不可能です。基本的に最初のスキーマに縛られてしまうので、一から作り直して正しく移行できることを自力で確認するか、アプリケーションレベルで扱うことになります。トランザクションが使えないACIDでないクエリが少し大きくなったときの速度も大したことはないと感じるメインで使っているデータベースでこんな目に遭っても構いませんか?私はイヤです。個人的にMongoDBの唯一の目玉機能と思えるのは、親ドキュメントに多数のドキュメントを埋め込めることぐらいです。それ以外の機能はおそらくPostgreSQLで用が足ります(それにセキュリティアップデートの面倒を見なければならないデータベースシステムが1つで済みます)。動的スコープは、他に打つ手がない場合にはよいパターン(permalink)Rubyでクロージャ(procやlambda)を定義すると、レキシカルなスコープや環境がクロージャにカプセル化されます。これは、コードのAという場所でprocを定義したとしても、コードのBという場所でそれを渡して呼び出したときに、procが定義されたAのレキシカルスコープ内で定義されているものであれば変数でも何でも参照できるということです。言い方を変えると「環境について閉じている」ということです。これを逆にしたらどうなるでしょうか。コードのAという場所でprocを1つ定義し、そこでprocを呼んでもまったく意味がないが、コードのBという場所でprocを呼びたい場合にクロージャのレキシカルスコープを変更することで、実行結果にBの環境が反映されるようにするとします。次の例をご覧ください。(NameError)クロージャの定義時点ではinternal_nameが定義されていないので、当然name2メソッドは失敗します。instance_execを使うとprocのバインディング(レキシカルスコープ)を再定義できます。これは、アプリのある部分に書いたコードを、まったく異なるコンテキストで実行できるということです。これはどんなときに便利なのでしょうか?このあたりをいろいろハックしてみましたが、非常に有用な使いみちの1つはRailsのルーティングでした。次のようなルーティングがあるとします。end上から以下のルーティングが生成されます。/api/v1/company_users/:id/api/v1/company_users/:company_user_id/posts/api/v1/company_users/:company_user_id/posts/:post_id/stats:company_user_idはどうやら不要なので、次のようにしてクライアント側での柔軟性を高めたいと思います。/api/v1/stats?user_id=:company_user_id&post_id=:post_idしかしAPIは既に本番で稼働していて変更は困難です。endルーティングの中にparamsがある?そのとおり!理由は、次のスニペットを使って、procのコンテキストをコントローラのコンテキストに再バインドしているからです。HashWithIndifferentAccess.new(params.to_unsafe_h.reshape(self))endこれで、このルートにuser_idを送信すると、このメソッドをbefore_filterとして追加することで、company_user_idとして追加されます。end...このテクニックをルーティング以外で使ったこともありますが、ほとんどは最後の手段としてです。コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。enno.jpを運営。実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。Storageを使ってはいけない(翻訳)#Tags#会社紹介#自社サービス#勉強会#Rails#PHP#デザイン#実績#週刊RailsウオッチCONTACTお気軽にご相談ください。(翻訳)CONTACTTechRachoでは、パートナーシップをご検討いただける方からのご連絡をお待ちしております。お気軽にご意見・ご相談ください。