Ruby on Rails開発でよくある問題と対策ポイント
はじめに
Ruby on Railsの「パターンとアンチパターン」シリーズも、いよいよ最終回です。これまで多くのトピックについて調査・執筆を重ねてきました。この記事では、長年にわたりRailsアプリケーションを構築・リリースする中で遭遇してきた、最も一般的な問題をまとめてご紹介します。
ここで取り上げる考え方は、コードのほぼすべての場面に当てはまるものです。Model-View-Controller(MVC)パターンに限定されない、汎用的な原則としてお読みください。RailsのMVCに関連するパターンやアンチパターンに興味がある方は、モデル、ビュー、コントローラに関する各記事もぜひご覧ください。
それでは、一般的な問題とその教訓を見ていきましょう。
自己中心的なオブジェクトとデメテルの法則
「デメテルの法則」は、Demeterプロジェクトに取り組んでいた開発者たちによって名付けられたヒューリスティック(経験則)です。その考え方は、「オブジェクトは一度に1つのメソッドだけを呼び出し、複数のメソッド呼び出しを連鎖させないようにすればよい」というものです。実践的には、次のようなコードになります。
# 悪い例
song.label.address
# 良い例
song.label_address
このようにすることで、songオブジェクトは住所情報がどこから来るのかを知る必要がなくなります。住所の取得はlabelオブジェクトの責務だからです。メソッドチェーンは1回にとどめ、オブジェクトを「自己中心的」に設計して、内部情報を直接共有するのではなくヘルパーメソッド経由で提供することが推奨されます。
幸いなことに、Railsではヘルパーメソッドを自前で書かなくても、delegateヘルパーを使えば済みます。
class Label < ApplicationRecord
belongs_to :song
delegate :address, to: :song
end
delegateが受け付けるオプションの詳細は公式ドキュメントで確認できますが、考え方も実装も非常にシンプルです。デメテルの法則を適用することで構造的な結合度を減らせます。さらに強力なdelegateを組み合わせれば、少ない行数で柔軟なオプション付きの実装が可能になります。
デメテルの法則とよく似たもう一つの概念が「単一責任の原則(SRP)」です。モジュール、クラス、関数はシステムの単一の部分に対してのみ責任を持つべきだという原則です。別の言い方をすると、次のようになります。
同じ理由で変更されるものは集める。異なる理由で変更されるものは分離する。
SRPの解釈は人それぞれ異なることがありますが、要は構成要素一つひとつに単一の責務を持たせるということです。Railsアプリケーションの規模が拡大するにつれてSRPを守るのは難しくなりますが、リファクタリングの際には常に意識しておきたいポイントです。
機能追加やコード量の増加に伴い、人は往々にして手軽な解決策に飛びつきがちです。続いては、その「手軽な修正」について考えてみましょう。
知り合いがいるから大丈夫?(本当にそのGemが必要?)
Railsがホットな話題だった頃は、オープンソース協業のブームが起き、あちこちで新しいRuby gemが登場していました(現在のJavaScriptライブラリの氾濫に似ていますが、規模はずっと小さいものでした)。
👆 出典:Module Counts
さて、よくあるアプローチとしては、既存のgemを探して自分の課題を解決するというものでした。
それ自体は何も悪いことではありませんが、gemの導入を決める前に、いくつかアドバイスをさせてください。
まず、自分自身にこう問いかけてみましょう。
- そのgemの機能のうち、実際に使うのはどれくらいか?
- もっと「シンプル」な、あるいはより新しくメンテナンスされている類似のgemはないか?
- 必要な機能なら、自信を持って簡単に自前で実装できるのではないか?
gemの全機能を使う予定がないなら、わざわざ導入する価値があるかどうかを冷静に評価しましょう。また、gemの実装が複雑すぎて、もっとシンプルに実装できると確信できるなら、独自のソリューションを選ぶのも得策です。
私がもう一つ重視しているのは、gemのリポジトリがどれほど活発かという点です。アクティブなメンテナーはいるか? 最後のリリースはいつか? といった点を必ず確認しています。
さらに、gemの依存関係にも注意が必要です。特定バージョンの依存関係に縛られてしまうのは避けたいところなので、必ずgemspecファイルを確認しましょう。RubyGemsのドキュメントで、gemのバージョン指定方法を理解しておくことをおすすめします。
gemの話が出たので、関連する興味深い現象も紹介しておきます。「NIH(Not Invented Here)症候群」です。Rails/Ruby界隈でもよく見られるもので、次のセクションで詳しく見ていきましょう。
NIH症候群(やっぱりあのGemが必要かもしれない?)
キャリアの中で何度か、「NIH症候群」に陥る人々(私自身も含めて)を目撃する機会がありました。これは「車輪の再発明」に似た考え方です。チームや組織が、自分たちでコントロールできないライブラリ(gem)を信頼できない場合があります。その信頼の欠如が、すでに世に出ているgemを作り直すきっかけになってしまうのです。
ただし、NIHを経験することが良い結果につながる場合もあります。社内ソリューションを自作するのは素晴らしいことで、特に既存のソリューションよりも優れたものを作れた場合はそうです。そのソリューションをオープンソースとして公開できれば、なおさら良いでしょう(Ruby on RailsやReactがその好例です)。しかし、再発明のための再発明をするのはやめましょう。車輪はすでに十分に優秀なのです。
このテーマはなかなか判断が難しいものです。もし同じ状況に陥ったら、次の質問を自分に投げかけてみてください。
- 既存のソリューションより優れたものを作れる自信はあるか?
- 既存のオープンソースソリューションがニーズと合わないなら、コントリビューションを通じて改善できないか?
- さらに言えば、そのオープンソースソリューションのメンテナーとなり、世界中の開発者の助けになれないか?
とはいえ、時に独自路線を選んで自らライブラリを作らざるを得ないこともあります。組織の方針によりオープンソースライブラリの利用が認められないケースもあるでしょう。しかし何をするにしても、「車輪の再発明」だけは避けるべきだと私は考えています。
監視員の過剰救助(例外のrescueしすぎに注意)
人は、本来意図していた範囲以上の例外をrescueしてしまう傾向があります。
このトピックはこれまでの内容よりコード寄りの話ですが、常識と思われながらも時折コード内で目にするアンチパターンです。例を挙げましょう。
begin
song.upload_lyrics
rescue
puts '歌詞のアップロードに失敗しました'
end
rescueする例外を指定しないと、想定していなかった例外まで捕捉してしまうことになります。
この例の場合、本当の問題はsongオブジェクトがnilであることかもしれません。その例外がエラートラッカーに報告されたとき、「アップロード処理に何か問題がある」と思い込んでしまう一方で、実際にはまったく別の原因でエラーが発生している可能性があるのです。
安全のため、例外をrescueする際は発生しうる例外のリストを必ず把握しておきましょう。どうしても把握できない場合は、広く捕捉するよりも狭く捕捉する方が賢明です。既知の例外のみをrescueし、それ以外は後の段階で適切に対応するようにしましょう。
要求が多すぎる(SQLクエリが多すぎる問題)
このセクションでは、Web開発におけるもう一つの典型的な問題、リレーショナルデータベース関連の問題を扱います。
1つのリクエスト内で大量のSQLクエリをデータベースに浴びせてしまうケースです。この問題はどうやって起こるのでしょうか? 複数のテーブルから複数のレコードを1リクエストで取得しようとしたときに起こりえます。しかし、最も頻繁に発生するのが悪名高い「N+1クエリ問題」です。
次のようなモデルを想像してください。
class Song < ApplicationRecord
belongs_to :artist
end
class Artist < ApplicationRecord
has_many :songs
end
あるジャンルの曲を複数表示し、それぞれのアーティスト名も一緒に表示したいとします。
songs = Song.where(genre: genre).limit(10)
songs.each do |song|
puts "#{song.title} by #{song.artist.name}"
end
このコードでは、10曲を取得するためにまず1つのSQLクエリが発行されます。その後、曲ごとにアーティストを取得するためのSQLクエリが1件ずつ実行されます。つまり合計11クエリです。
表示する曲をもっと増やしたらどうなるでしょうか? データベースには大きな負荷がかかり、全アーティストの取得に多大なコストがかかることになります。
代わりに、Railsのincludesを使ってみましょう。
songs = Song.includes(:artist).where(genre: genre).limit(10)
songs.each do |song|
puts "#{song.title} by #{song.artist.name}"
end
includesを使えば、表示する曲の数にかかわらずSQLクエリはたった2件で済むようになります。とても便利ですね。
SQLクエリが多すぎる問題を診断する一つの方法は、開発環境でのログ確認です。同じテーブルからデータを取得する似たようなSQLクエリが連続して表示されていたら、そこで何か怪しいことが起きています。だからこそ、開発環境ではSQLログを有効にすることを強くおすすめします。また、Railsは冗長クエリログ(verbose query logs)にも対応しており、クエリがコードのどこから呼び出されたのかを表示できます。
ログを目視で確認するのが好みでない方や、もっと本格的なツールをお探しの方には、AppSignalのパフォーマンス計測およびN+1クエリ検出機能をお試しください。問題がN+1クエリに起因するものかどうかを的確に示してくれます。
まとめ
このブログシリーズをお読みいただき、ありがとうございました。Railsにおけるパターンとアンチパターンの紹介から始まり、MVCパターン内での詳細、そして今回の一般的な問題まで、興味深い旅にお付き合いいただけたことを嬉しく思います。
皆さんが多くを学べたこと、少なくとも既に持っている知識の整理と定着につながったことを願っています。すべてを暗記しようと気負う必要はありません。困ったときは、いつでもこのシリーズに戻って参照できます。
パターンにもアンチパターンにも、必ず出会うことになるでしょう。この世界(特にソフトウェア工学)は完璧ではないからです。それも心配いりません。
パターンとアンチパターンを習得することは、優れたソフトウェアエンジニアへの第一歩です。しかしさらに上達する鍵は、それらのパターンや型をいつ破るべきかを知ることです。完璧な解決策など存在しないのですから。
改めてお読みいただき、ありがとうございました。それでは次の記事でお会いしましょう!
P.S. Ruby Magicの最新記事をいち早くお読みになりたい方は、Ruby Magicニュースレターをご購読ください。記事を見逃すことはありません!
-
スキャナーのよくあるトラブル6選|原因別トラブルシューティングと解決策ガイド
プロジェクトを予定より早く仕上げたのに、スキャンや印刷の遅さが原因で提出が間に合わなかった——ほど悔しいことはありません。スキャナーには、速度の問題以外にもユーザーを悩ませるさまざまな不具合が存在します。本記事では、代表的なスキャナーのハードウェアトラブルを6つ取り上げ、それぞれの原因と具体的な対処法・解決策をわかりやすく解説します。 スキャナーの主なトラブルとトラブルシューティング 機器の品質は作業効率に直結します。新品で傷のないプラスチックキャリアであっても、状態によってはスキャン品質を低下させることもあります。ここでは、機器を常に最適な状態で使い続けるためのポイントをご紹介します。 1.
-
Ruby on Railsとは?初心者にもわかる仕組み・魅力・学び方を徹底解説
Ruby on Railsとは? Ruby on Rails(略称:RoR)は、世界で最も人気のあるオープンソースのWebアプリケーションフレームワークです。プログラミング言語「Ruby」をベースに構築されており、シンプルなサイトから大規模で複雑なサービスまで、幅広いWebアプリケーションの開発を支援します。 そもそもフレームワークとは? フレームワークとは、ソフトウェア開発の際に土台となる構造を提供してくれるコードやツール、ユーティリティの集合体です。あらかじめ用意された構造に沿ってコードを書くことで、プログラムが整理され、保守性も高まります。正しく使いこなせるようになれば、開発作業は格段に