もう怖くない!Rubyのモンキーパッチを安全に使いこなす3つの方法
モンキーパッチ。Rubyを初めて触ったとき、その強力さに驚かされることでしょう。コアクラスに直接メソッドを追加できるのですから。Time.now.advance(days: -1)と書かずに済み、1.day.agoと書ける。これこそが、Rubyを読み書きする楽しさの源泉です。しかし……。
パッチがHashを書き換えたせいで、奇妙なバグに遭遇する。
実際にどのコードが実行されたのか分からなくなり、壊れたときにデバッグできない。
そしてついに気づくのです。すべての問題は6ヶ月前、たった5文字コードを短くするためにEnumerableをモンキーパッチしたあの時に、すでに生まれていたのだと。
とはいえ、代替案はあるのでしょうか? 利便性を一切諦めるしかないのでしょうか? GroupableArray.new([1, 2, 3, 4]).in_groups_of(2)のような冗長なコードを書く日々に戻る? blank?がRuby本体に取り込まれるまで、快適なユーザー入力処理を我慢し続ける?
モンキーパッチを完全に手放したいわけではないはずです。では、どうすれば「次に見たときに自分をクビにしたくなる」ようなモンキーパッチを避けられるのでしょうか?
1. モジュールにまとめる
クラスをモンキーパッチするとき、単純にクラスを再オープンしてパッチを押し込むのはやめましょう。
class DateTime
def weekday?
!sunday? && !saturday?
end
end
なぜダメなのでしょうか? 理由はいくつもあります。
複数のライブラリが同じメソッドをパッチしても、それを検知できません。
先に定義されたパッチは黙って上書きされ、永遠に消え去ります。
エラー発生時に、問題が
DateTimeの中で起きたように見えてしまいます。技術的には間違いではありませんが、原因特定の役には立ちません。
パッチを無効化するのが面倒になります。
パッチ全体をコメントアウトするか、パッチファイルのrequire自体をスキップするしかありません。
万が一
require 'date'を忘れたままパッチを実行すると、DateTimeを拡張するどころか、うっかり再定義してしまう危険があります。
代わりに、モンキーパッチはモジュールとして定義しましょう。
module CoreExtensions
module DateTime
module BusinessDays
def weekday?
!sunday? && !saturday?
end
end
end
end
こうすることで、関連するパッチをひとまとめに整理できます。エラーが出たときも、問題のコードがどこから来たのか一目瞭然です。さらに、グループ単位で取り込むこともできます。
# 実際にDateTimeへパッチを適用
DateTime.include CoreExtensions::DateTime::BusinessDays
パッチが不要になったら、この1行をコメントアウトするだけでOKです。
2. パッチの置き場所を統一する
コアクラスをモンキーパッチするということは、RubyのコアAPIを拡張するということです。コアパッチを持つアプリケーションは、それぞれ少しずつ独自の顔を持つことになります。だからこそ、新しいコードベースに参加したときに素早く変更点を把握できる仕組みが必要です。自分のモンキーパッチがどこに住んでいるのか、常に知っておかなければなりません。
私は主にRailsの慣習に従っています。パッチはlib/core_extensions/クラス名/グループ名.rbに配置します。先ほどのパッチなら:
module CoreExtensions
module DateTime
module BusinessDays
def weekday?
!sunday? && !saturday?
end
end
end
end
lib/core_extensions/date_time/business_days.rbに置きます。
新しく加わった開発者でも、lib/core_extensions以下のファイルを眺めるだけで、プロジェクトがRubyにどんな拡張を加えたのかをすぐ学べます。せっかく書いた便利なメソッドが「謎の存在」として邪魔になるどころか、実際に活用されるようになるのです。
3. エッジケースを考慮する
Enumerableになぜsumメソッドがないのか、私には分かりませんでした。[1, 2, 3].sumや["a", "b", "c"].sum、[Article.new, Article.new, Article.new].sumと書けたら……あれ?
※ちなみに、この記事の執筆当時はsumは存在しませんでしたが、Ruby 2.4以降ではEnumerable#sumが標準で提供されています。ただし、引数なしでオブジェクト同士を足す挙動には注意が必要です。
クラスをモンキーパッチするとき、私たちは通常「楽にしたいこと」ひとつだけを考えています。数値の合計を計算したい一心で、配列には数値以外のオブジェクトも入ることを忘れてしまうのです。
今の自分にとっては理にかなっています。ハッシュの平均を計算しようなどと思いませんよね。しかし、呼び出すと時々平気で失敗するメソッドをオブジェクトに生やしておくと、後で必ず自分自身を混乱させることになります。
対処法はいくつかあります。良い順に挙げてみましょう。
想定外の入力を合理的に処理する。
パッチが文字列を扱うなら特に有効です。まず
to_sを呼び出せば、ほぼどんなオブジェクトからでも妥当な結果を得られます。また、『Confident Ruby』は多様な入力への対処法を豊富に教えてくれる良書です。エラーを分かりやすい形で通知する。
想定外の入力を見つけたら、親切なメッセージ付きの
ArgumentErrorを投げるだけで十分です。他の人が謎のNoMethodErrorを解読してくれることに期待してはいけません。コメントで想定する入力を文書化する。
可能なら前の2つの方法の方が優れています。しかしパッチ内部でエッジケースをチェックできないなら、せめて文書化しておきましょう。そうすれば、呼び出し側が「原因はあのパッチだ」と突き止めたとき、作者の意図を推測できるようになります。
番外編:私の大好きなモンキーパッチ
最後に、私の大のお気に入りのモンキーパッチをご紹介します。Hash#string_mergeです。
module CoreExtensions
module Hash
module Merging
def string_merge(other_hash, separator = " ")
merge(other_hash) {|key, old, new| old.to_s + separator + new.to_s}
end
end
end
end
{}.string_merge({:class => "btn"}) # => {:class=>"btn"}
h = {:class => "btn"} # => {:class=>"btn"}
h.string_merge({:class => "btn-primary"}) # => {:class=>"btn btn-primary"}
HTML要素にCSSクラスを組み合わせる作業が、格段に快適になります。
賢いモンキーパッチングのために
コアクラスのモンキーパッチは、すべてが悪というわけではありません。上手に使えば、コードはより「Rubyらしく」なります。しかし、Rubyの鋭い刃物と同じように、扱うときには一段深い注意が必要なのです。
パッチを一箇所に集め、モジュールとしてグループ化し、想定外の入力に備える。この3つを守れば、モンキーパッチは最大限安全な状態で保てます。
あなたが今まで書いた(または見かけた)最高のモンキーパッチは何ですか? ぜひコメントで教えてください!
-
Wordを使わずにExcelだけで差し込み印刷(メールマージ)を行う2つの実践的な方法
Wordを使わずにExcelで差し込み印刷(メールマージ)を実現する方法をお探しですか?この記事はまさにそのためのものです。何らかの理由でMicrosoft Wordが使えない環境では、差し込み印刷の作業が難しく感じられるかもしれません。しかし、決して不可能ではありません。差し込み印刷の操作にはWordは必須ではないのです。この記事では、Excelのみで差し込み印刷を行うための2つのVBAマクロをご紹介します。 差し込み印刷(メールマージ)とは? 個人や企業が、宛名やメールアドレスなど一部の内容だけを変えた大量のメールを送りたい場合があります。そのようなとき、ひな形(テンプレート)を作成し、編
-
データを失わずにWindows 11を工場出荷時設定にリセットする3つの方法
Windows 11のパソコンで問題が発生し、いくつかの対処法を試しても解決しないことがあります。そんなときの最終手段として、Windows 11を工場出荷時設定にリセット(初期化)することで、問題を解決しシステムのパフォーマンスを改善できる場合があります。Windows 11の動作が遅い、または不具合が頻発する場合は、PCを初期状態に戻すことで多くの一般的な問題を解消し、全体的なパフォーマンスを向上させることができます。この記事では、データを失うことなく「このPCをリセット」機能を使ってWindows 11を初期状態に戻す方法を詳しく解説します。 Windows 11の「このPCをリセット