同じメソッドの上書きを狙ってaliasとprependを併用すると無限ループに陥る可能性がある

タイトルの通りなので、Rails 5 への移行時には特に気を付けましょう。

変更は独立させて小さく適用していきたい

自分はフリーランスとして働いており、Rails のバージョンアップや設計改善の仕事を探してよく請けているのですが、その中でも最近は Rails 4 から 5 へのバージョンアップの仕事を担当する機会が多くあります。

Rails のバージョンアップ時の作戦として、まずはアプリケーション側のコードを現在のバージョンでも次の移行先のバージョンでも動くような実装に変更し、問題がないことを確認し、その上で次のバージョンに移行するという作戦を取ることがよくあります。実際には前者の変更を更に細かく分け、少しずつ本番環境に適用していくことが多いです。1つ1つの変更を小さく分割することで、確認を行いやすくし、問題発生時のリスクを最小限に抑えるためです。

alias_method_chain は非推奨になった

さて、Rails 4 から Rails 5 の間の大きな変更の1つとして、ActiveSupport::CoreExtensions::Module#alias_method_chain が非推奨になり、代わりに Module#prepend を利用する実装に置き換えることが推奨されるようになりました。このことを受けて、例えばこれまで alias_method_chain を利用していたライブラリの多くは、prepend を利用する実装に置き換えられていっています。

prepend と alias を併用しないように気を付けよう

これに対応して、Rails を利用するアプリケーション側でも、利用しているライブラリのバージョンを少しずつアップデートしていく必要があるのですが、このときこの記事のタイトルに掲げた内容が問題になります。すなわち、同じメソッドを alias_method_chain で上書きするライブラリが2つ存在する場合、片方だけ prepend で実装するように変更してしまうと、その実行順序によっては問題が起こり、無限ループに陥ってしまう可能性があるということです。詳細としては、以下のページ内での説明が分かりやすいかもしれません。

Bug #11120: Unexpected behavior when mixing Module#prepend with method aliasing - Ruby trunk - Ruby…
Redminebugs.ruby-lang.org

例えば、私見ですが、ActiveRecord のためのライブラリの実装などは、この手の方法で上書きされていることが多いように思います。先に述べたように細かく変更を加えていきたいにも関わらず、これが理由で同時に加えなければならない変更が発生してしまうため、結果的に1つの変更がどうしても大きくなってしまいます。特に効果的な対策を説明できる訳ではありませんが、気を付けましょうという内容でした。

prepend と alias を併用して FizzBuzz を動かそう

さて、prepend と alias を併用すると意図せず無限ループが発生する可能性があることを利用し、ここでは FizzBuzz を書いてみることにしました。

prepend と alias を利用した fizzbuzz

上記のコードでは、

  • 3 で割り切れるかどうか判定する
  • 5 で割り切れるかどうか判定する
  • 結果を標準出力する
  • 内部状態をリセットする

という処理をそれぞれ module に分割し、責務の分割を狙っています (そういうストーリーでいきます)。prepend を利用することで、以下のような継承ツリーが生まれます。

Object|`-FizzBuzz | `-Printable | `-Buzz | `-Fizz | `--Clearable

super で根方向に1つ遷移し、alias されたメソッド呼び出しで葉に遷移するというように考えることで、連続するメソッド呼び出しを状態遷移のように捉え、1ループで1つの数に対応するような設計にできました。

START
|
v
Clearable#call <-.
| |
(super) |
| |
v |
Fizz#call |
| |
(super) |
| |
v |
Buzz#call |
| |
(super) |
| |
v |
Printable#call |
| |
(super) |
| (aliased from #step)
v |
FizzBuzz#call ---'
|
v
END

以上です。prepend と alias とは仲良くやっていきましょう。