Railsのパターンとアンチパターン5: 一般的な問題と、その教訓(翻訳)
Ruby on Railsのパターンとアンチパターンシリーズの最終回へようこそ。本シリーズのトピック執筆と調査は苦労の連続でした。今回は、私が長年Ruby on Railsアプリケーションを構築・リリースしたときに踏んだ最も一般的な問題を扱います。
今回ご紹介する内容は、コードのほぼどの部分でも通用します。したがって、MVCパターンに限らず、一般的に通用するアイデアとお考えいただければ結構です。Rails MVCのパターンやアンチパターンについては、過去記事のモデル編、ビュー編、コントローラ編をご覧ください。
それでは一般的な問題と教訓について見ていきましょう。
利己的なオブジェクトとデメテルの法則
デメテルの法則とは、ある研究グループが取り組んでいた「デメテルプロジェクト」にちなんで名付けられたヒューリスティクス(発見的方法)です。この法則の要点は、「オブジェクトが一度に呼び出すメソッドが1個であり、複数のメソッドをチェインしていない限り、そのオブジェクトは問題ない」というものです。実際の意味は、次のようになります。
# Bad
song.label.address
# Good
song.label_address
上のGoodでは、address
がどこにあるかをsong
オブジェクトが知っておく必要がなくなります。このaddress
はlabel
オブジェクトの管轄であり、song
オブジェクトはaddress
について関知しません。このように、メソッド呼び出しのチェインは1回のみとし、オブジェクトが「自分のことしか考えない」ように、つまりオブジェクトを「利己的」にしましょう。オブジェクトのすべての情報をメソッドチェインで直接共有せず、ヘルパーメソッド経由で共有するようにしましょう。
ありがたいことに、Railsではヘルパーメソッドそのものを書く必要はありません。以下のようにdelegate
ヘルパーを利用できます。
def Song < ApplicationModel
belongs_to :label
delegate :address, to: :label
end
delegete
のAPIドキュメントでこのヘルパーのオプションを調べればもっといろんなことができますが、基本的なアイデアと実行はかなりシンプルです。デメテルの法則を適用して、構造的な癒着を減らしましょう。強力なdelegate
ヘルパーと併用すれば、より少ない行数でより多くのオプションを利用しながら癒着を減らせるようになります。
デメテルの法則と非常に近い関係にあるのは、単一責任の原則(SRP: Single-responsibility Principle)です。単一責任の原則は、「モジュールやクラスや関数が負う責務は、システムの1つの部分に限られるべき」というものです。言い方を変えれば次のようになります。
同じ理由で変更されるものは一箇所に集めよ1。変更の理由が異なるものは切り離せ。
単一責任の原則は人によって解釈がばらつきがちですが、その考え方は「建築ブロックの役割は1つに絞っておくこと」というものです。単一責任の原則を守るのは、Railsアプリの規模が大きくなるに連れて難しくなるかもしれませんが、リファクタリングの際にはご注意ください。
機能を追加したりLOC(コードの行数)が増えたりすると、つい焦って手っ取り早いソリューションに飛びついてしまう人々が多いようです。そこで、そうした手っ取り早いソリューションがどのようなものかを見ていきましょう。
「ぼくはこの人知ってるけどね」問題(そのgemは本当に必要?)
その昔、Railsがアツい話題になっていた頃、オープンソースのコラボレーションがブームになり、新しいRuby gemがあちこちで生まれました(昨今のJavaScriptライブラリの盛り上がりと似ていますが、それよりはずっと小規模です)。
👆 Information from Module Counts
それはともかく、当時の問題解決方法といえば、既存のgemを探し出すのが常套手段でした。
それ自体は別に構わないのですが、皆さんが何らかのgemをインストールする決定を下す前に、私からいくつかアドバイスしておきたいと思います。
最初に以下の項目をセルフチェックしておきましょう。
- そのgemのどの機能を使うかを調べたか?
- 機能の似た「よりシンプル」なgemや最新のgemがあるかどうかを調べたか?
- その機能は自分が自信を持って楽に実装できるか?2
あるgemの機能をすべて使うつもりがなければ、gemではなく自分で実装する価値があるかどうかを検討しましょう。gemの実装が無駄に複雑で、自分ならもっとシンプルに実装できると思えるなら、カスタム実装を選択しましょう。
私はgemを検討するときに、「そのgemリポジトリが今も活発にメンテナンスされているかどうか」、「直近のリリース日はいつなのか」についてもチェックします。
また、gemの依存関係にも注意を払っておく必要があります。依存関係の特定バージョンにロックされるのは嬉しくないので、必ずGemfile.specファイルをチェックしておきましょう。詳しくはRubyGemsでgemバージョンを指定する方法をご覧ください。
gemの話題ついでに、次のセクションでは、RailsやRubyの世界に当てはまる「よそのgem」(Not Invented Here)現象に私が出くわした話をしましょう。
「よそのgem」問題(そのgemは本当に本当に必要?)
私はこれまでのキャリアの中で、多くの人々が(私もですが)「よそのgem」症候群に陥ってしまう姿を何度となく経験する機会がありました。これは、いわゆる「車輪の再発明」と形が似ています。チームや組織は、自分たちがコントロールできないライブラリ(gem)を信頼しないことがあります。外部のgemを信頼できないせいで、既に存在するgemを再発明するきっかけになる場合もあります。
ときには「よそのgem」問題に遭遇することが吉と出る場合もあります。ソリューションを内製できれば、特に外部のソリューションよりも改善されれば素晴らしいことでしょうし、そのソリューションをオープンソース化すればさらに素晴らしいことです(Ruby on RailsやReactを見ればおわかりでしょう)。しかし、「よそのgem」を使わないために車輪を再発明するのはやめておきましょう。車輪そのものが既に相当素晴らしいのですから。
このテーマは一筋縄ではいきません。もしそうした状況に囚われてしまったら、以下の項目でセルフチェックしてみてください。
- 既存のソリューションより優れたものを内製できる確信はあるか?
- 既存のオープンソースソリューションが自分たちの必要なものと違う場合、オープンソースに貢献して改善することは可能か?
- さらに、自分たちがそのオープンソースソリューションのメンテナーになって多くの開発者を幸せにすることは可能だろうか?
しかし、ときにはライブラリを手作りする以外の選択肢が取れないこともあります。組織がオープンソースライブラリのライセンスを好んでいなければ、内製するしかないでしょう。しかしどの道を選ぶにしても、車輪の再発明は避けたいものです。
ただいま監視中3(例外のrescue
過剰問題)
例外のrescue
は、当初の意図を超えて増えがちです。
この話題は先ほどよりもコード寄りです。「何を今さら」と思われるかもしれませんが、たとえば以下のようなコードをたまに見かけることがあります。
begin
song.upload_lyrics
rescue
puts 'Lyrics upload failed'
end
rescue
したい例外を具体的に指定していないと、想定外の例外までキャッチされてしまいます。
上のコード例の場合、song
オブジェクトがnil
になることが問題になる可能性があります。この例外がエラートラッカーに出力されると、おそらくアップロード周りに何か問題があるのかなと思うでしょう。しかし実際にはまるで違うことが起きているかもしれません。
安全のため、例外をrescue
するときには、発生する可能性のあるすべての例外のリストを用意しましょう。何らかの理由で一部の例外をキャッチできない場合は、rescue
過剰よりもrescue
不足の方がましです。リストにある例外はrescue
し、それ以外は後回しにしましょう。
質問が多すぎる(SQLクエリ過剰問題)
本セクションでは、Web開発のもうひとつの側面であるリレーショナルデータベースの問題を見ていきます。
1個のリクエストで大量のSQLクエリが発生して怒涛のようにWebサーバーに押し寄せる問題は、どのようにして発生するのでしょうか。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曲を取得するときにSQLクエリを1つトリガーします。続いて、1曲ごとにアーティストを取得するSQLクエリが実行されます。つまりクエリ数は合計11個になります。
読み込む曲数が増えた場合を想像してみましょう。アーティストをすべて取得しようとするときにデータベースの負荷が増大します。
対抗手段として、Railsのincludes
メソッドを以下のように使います。
songs = Song.includes(:artists).where(genre: genre).limit(10)
songs.each do |song|
puts "#{song.title} by #{song.artist.name}"
end
includes
を使うと、SQLクエリはわずか2つに減りました。曲数がどれだけ増えても変わりません。大したメソッドですね。
SQLクエリが多すぎる問題を診断する方法のひとつはdevelopment環境にあります。似たような複数のSQLクエリが同じテーブルからデータをフェッチしているのを見かけたら、そこで何か怪しいことが起きているものです。そういうわけで、development環境ではSQLログ出力をオンにしておくことを強くおすすめします。Railsでは詳細なクエリログ出力もサポートされており、クエリがコードのどの部分から呼び出されているかを表示できます。
ログを読むのが苦手な方や、もっと本格的なツールが欲しい方は、AppSignalのパフォーマンス測定 + N+1 クエリ検出サービスをぜひお試しください。以下のように、問題の原因がN+1クエリかどうかを判定する優れた指標を得られます。
まとめ
本シリーズ記事を最後までお読みいただき、ありがとうございます。Railsのパターンやアンチパターンの紹介からRails MVCパターンの内部の探検までを扱い、最終回でRailsの一般的な問題を取り上げるという好奇心あふれる旅に参加できたことを嬉しく思っています。
皆さんは本シリーズから多くのことを学んだか、少なくとも既存の知識を更新して定着できたかと思います。これらは頑張って丸暗記するような知識ではありません。困ったときにはいつでも本シリーズの記事に立ち返ればよいのです。
この世界(特にソフトウェアエンジニア界隈)は理想からほど遠いので、いずれ皆さんも、必ずこうしたパターンとアンチパターンの両方に出会うことでしょう。どちらであろうと心配は無用です。
パターンとアンチパターンを身につければ、優秀なソフトウェアエンジニアになれます。しかしそこからさらにレベルアップするには、パターンやひな形をあえて破るタイミングを見極めることです。この世に完璧なソリューションはありません。
重ねてお礼を申し上げます。本シリーズをお読みいただきありがとうございました。次回作でまたお会いしましょう!
⚓ お知らせ
Rubyのマジックに関する記事が公開されたらすぐ読みたい方は、元記事末尾のフォームにて「Ruby Magic」ニュースレターをご購読いただければ、新着記事を見逃さずに読めるようになります。
概要
原著者の許諾を得て翻訳・公開いたします。
画像は元記事からの引用です。