Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Railsのパターンとアンチパターン5: 一般的な問題と、その教訓(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

画像は元記事からの引用です。

Railsのパターンとアンチパターン5: 一般的な問題と、その教訓(翻訳)

Ruby on Railsのパターンとアンチパターンシリーズの最終回へようこそ。本シリーズのトピック執筆と調査は苦労の連続でした。今回は、私が長年Ruby on Railsアプリケーションを構築・リリースしたときに踏んだ最も一般的な問題を扱います。

今回ご紹介する内容は、コードのほぼどの部分でも通用します。したがって、MVCパターンに限らず、一般的に通用するアイデアとお考えいただければ結構です。Rails MVCのパターンやアンチパターンについては、過去記事のモデル編ビュー編コントローラ編をご覧ください。

それでは一般的な問題と教訓について見ていきましょう。

利己的なオブジェクトとデメテルの法則

デメテルの法則とは、ある研究グループが取り組んでいた「デメテルプロジェクト」にちなんで名付けられたヒューリスティクス(発見的方法)です。この法則の要点は、「オブジェクトが一度に呼び出すメソッドが1個であり、複数のメソッドをチェインしていない限り、そのオブジェクトは問題ない」というものです。実際の意味は、次のようになります。

# Bad
song.label.address

# Good
song.label_address

上のGoodでは、addressがどこにあるかをsongオブジェクトが知っておく必要がなくなります。このaddresslabelオブジェクトの管轄であり、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クエリかどうかを判定する優れた指標を得られます。

AppSignal's N+1 query detection

まとめ

本シリーズ記事を最後までお読みいただき、ありがとうございます。Railsのパターンやアンチパターンの紹介からRails MVCパターンの内部の探検までを扱い、最終回でRailsの一般的な問題を取り上げるという好奇心あふれる旅に参加できたことを嬉しく思っています。

皆さんは本シリーズから多くのことを学んだか、少なくとも既存の知識を更新して定着できたかと思います。これらは頑張って丸暗記するような知識ではありません。困ったときにはいつでも本シリーズの記事に立ち返ればよいのです。

この世界(特にソフトウェアエンジニア界隈)は理想からほど遠いので、いずれ皆さんも、必ずこうしたパターンとアンチパターンの両方に出会うことでしょう。どちらであろうと心配は無用です。

パターンとアンチパターンを身につければ、優秀なソフトウェアエンジニアになれます。しかしそこからさらにレベルアップするには、パターンやひな形をあえて破るタイミングを見極めることです。この世に完璧なソリューションはありません。

重ねてお礼を申し上げます。本シリーズをお読みいただきありがとうございました。次回作でまたお会いしましょう!

お知らせ

Rubyのマジックに関する記事が公開されたらすぐ読みたい方は、元記事末尾のフォームにて「Ruby Magic」ニュースレターをご購読いただければ、新着記事を見逃さずに読めるようになります。

関連記事

Railsのパターンとアンチパターン1: 概要編(翻訳)

Railsのパターンとアンチパターン2: モデル編とマイグレーション(翻訳)

Railsのパターンとアンチパターン3: ビュー編(翻訳)

Railsのパターンとアンチパターン4: コントローラ編(翻訳)

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)


  1. 原文は聖書をもじったような言い回しになっています。 
  2. わざわざgemを使わなくても手作りできる機能かというチェックです。例: シンプルなパンくずリスト 
  3. 原文「lifegard on duty」は、プールやビーチなどで監視台の表示によく使われます。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。