こんにちは、鈴木です。
既存のメソッドの動作を少しだけ変更したい、という場合に使われる常套手段をご紹介します。
とあるシステムのログ出力モジュール
とあるシステムでログ出力機能が必要になり、以下のような Logging モジュールを作成したとします。
1 2 3 4 5 6 7 |
module Logging def log(message) puts message end end |
log メソッドに文字列を渡すと、それがログ出力されます。
1 2 |
include Logging log('Hello') |
とすると、
1 |
Hello |
と表示されます。
ログにタイムスタンプを付加したい
出力されるログにタイムスタンプが含まれたほうが嬉しいのですが、どうしましょう。
以下のように呼び出し側で苦労はしたくありません。
1 2 |
log("#{Time.now} メッセージ1") log("#{Time.now} メッセージ2") |
alias_method で置き換える
以下のように alias_method を使用して既存のメソッドを置き換えることで実現できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
module Logging def log(message) puts message end # (1) タイムスタンプを付加して既存の処理に渡すメソッドを定義する. def log_with_timestamp(message) log_without_timestamp("[#{Time.now}]#{message}") end # (2) log_without_timestamp で既存の log を参照できるようにする. alias_method :log_without_timestamp, :log # (3) log は log_with_timestamp を参照するようにする. alias_method :log, :log_with_timestamp end # 動作確認. include Logging log('Hello') |
やるべきことは 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 を使うと、より綺麗に書くことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
module Logging def log(message) puts message end def log_with_timestamp(message) log_without_timestamp("[#{Time.now}]#{message}") end # ここが変わった. alias_method_chain :log, :timestamp end # 動作確認. include Logging log('Hello') |
コメントで「# ここが変わった.」と書いた部分に注目してください。違いはこの部分だけです。
alias_method_chain を用いると
1 2 |
alias_method :log_without_timestamp, :log alias_method :log, :log_with_timestamp |
というパターンを
1 |
alias_method_chain :log, :timestamp |
と書き換えることができます。
行数が短くなっただけではなく、コードを見た時に意図が伝わりやすくなったのではないでしょうか。
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 を理解した今なら簡単ですね。
以下のように実現できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
module Logging def log(message) puts message end def log_with_timestamp(message) log_without_timestamp("[#{Time.now}]#{message}") end alias_method_chain :log, :timestamp # ここを追加. def log_with_pid(message) log_without_pid("[#{Process.pid}]#{message}") end alias_method_chain :log, :pid end # 動作確認. include Logging log('Hello') |
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 のその心は、いつまでも忘れずにいたいものです。
Comments
質問させてください。
とありますが、これはlog("hoge")は呼び出せないということでしょうか。
(2)の処理の段階で試してみたところ
log("hoge") => # "hoge"
と出力されました。
ruby2.2.3で試してみましたが、
古いバージョンだとそのような挙動になるのでしょうか。
yusuke さん。ご質問いただいたところですが、記事内容の間違いです。申し訳ありません。
鈴木様。ご対応ありがとうございます!
記事の内容を訂正いたしました。
ご指摘いただけたことで間違いに気付くことができました。ありがとうございました。