Rails: alias_method_chain: 既存の処理を修正する常套手段

Pocket

こんにちは、鈴木です。

 

既存のメソッドの動作を少しだけ変更したい、という場合に使われる常套手段をご紹介します。

 

とあるシステムのログ出力モジュール

とあるシステムでログ出力機能が必要になり、以下のような Logging モジュールを作成したとします。

log メソッドに文字列を渡すと、それがログ出力されます。

とすると、

と表示されます。

 

ログにタイムスタンプを付加したい

出力されるログにタイムスタンプが含まれたほうが嬉しいのですが、どうしましょう。

以下のように呼び出し側で苦労はしたくありません。

 

alias_method で置き換える

以下のように alias_method を使用して既存のメソッドを置き換えることで実現できます。

やるべきことは 3 つあります。

  • (1) 最初に、渡された文字列にタイムスタンプを付加して既存の処理に渡す log_with_timestamp メソッドを定義します。このとき、既存の処理を呼び出すときは log ではなく log_without_timestamp としておきます。
  • (2) 次に、alias_method で log_without_timestamp を log のエイリアスとします。この時点でオリジナルの処理は log_without_timestamp を通してしか呼び出すことはできなくなります。(2016-05-29: 記載内容に誤りがありましたので修正しました。打ち消し線が入っている箇所は以下の (3) のところに移動しました。)
  • (3) 最後に、alias_method で log は log_with_timestamp を参照するようにエイリアスを作成します。この時点でオリジナルの処理は log_without_timestamp を通してしか呼び出すことはできなくなります。

こうすることで、既存の処理をラップする形で独自の処理を追加することができます。

 

alias_method_chain で置き換える

ActiveSupport が提供する alias_method_chain を使うと、より綺麗に書くことができます。

コメントで「# ここが変わった.」と書いた部分に注目してください。違いはこの部分だけです。

alias_method_chain を用いると

というパターンを

と書き換えることができます。

行数が短くなっただけではなく、コードを見た時に意図が伝わりやすくなったのではないでしょうか。

alias_method_chain には以下のルールがあります。

  • 新しいメソッドは xxx_with_xxx という名前で定義する必要がある。
  • オリジナルのメソッドは xxx_without_xxx という名前に置き換えられる。

最初の alias_method を用いた例でも上記のルールを満たすようにしていましたが、alias_method にはそのような決まりはなく、メソッド名は自由に決めることができます。

とはいえ自由すぎると、「既存の処理を置き換えたい」→「alias_method を使う」→「メソッド名はどうしよう・・」と迷います。

その点 alias_method_chain を使う場合は「既存の処理を置き換えたい」→「alias_method_chain を使う」→「メソッド名は _with_xxx/_without_xxx」と迷うことがありません。with_xxx/without_xxx という命名ルールは分かりやすい規律を生み出す良い制約だと思います。

 

ログにプロセス ID も付加したい

今度はログにプロセス ID も付加したくなりました。どうしましょう。

今の状態でログにタイムスタンプが付加されるようになっていますので、この動作を壊さないように実現したいです。

alias_method_chain を理解した今なら簡単ですね。

以下のように実現できます。

log_with_pid というメソッドを定義して、「alias_method_chain :log, :pid」とするだけです。

このように alias_method_chain で置き換えられたものを、さらに alias_method_chain で置き換える、ということが簡単に出来ます。

デザインパターンの一つに Decorator パターンというものがあります。 Decorator パターンは、既存のクラスをデコレートして機能を追加するものですが、alias_method_chain を使った方法はメソッドに対する Decorator パターンと考えても良さそうですね。

 

Ruby2.0 の Module.prepend に置き換えられる運命かも

alias_method_chain を見てきたわけですが、Ruby2.0 で登場する Module.prepend に置き換えられる運命かもしれません。(参照: 「Ruby2.0のModule.prependは如何にしてalias_method_chainを撲滅するのか!?」)

とはいえ、定形パターンに規律を与え続けてきた alias_method_chain のその心は、いつまでも忘れずにいたいものです。

 

Pocket

Comments

  • yusuke  On 2016年5月28日 at 13:36

    質問させてください。

    この時点でオリジナルの処理は log_without_timestamp を通してしか呼び出すことはできなくなります。

    とありますが、これはlog("hoge")は呼び出せないということでしょうか。

    (2)の処理の段階で試してみたところ
    log("hoge") => # "hoge"
    と出力されました。

    ruby2.2.3で試してみましたが、
    古いバージョンだとそのような挙動になるのでしょうか。

    • 鈴木 圭  On 2016年5月29日 at 19:40

      yusuke さん。ご質問いただいたところですが、記事内容の間違いです。申し訳ありません。

      • yusuke  On 2016年6月25日 at 12:44

        鈴木様。ご対応ありがとうございます!

    • 鈴木 圭  On 2016年5月29日 at 19:49

      記事の内容を訂正いたしました。
      ご指摘いただけたことで間違いに気付くことができました。ありがとうございました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です