昨日、ここデンバーの会社で Ruby 開発者のポジションの面接を受けていたとき、面接官がメタプログラミングの良い課題を提示してくれました:自動メソッド監査人を書くようにとのことでした。 このようなものです:
それは、次のような出力を生成するでしょう:
私は、機能するソリューションを考え出しましたが、Object#send
に対する危険なハックを含む可能性がありました。 解決策を発表する前に、私は「これはおそらく悪い考えだが…」と言いましたが、そのとおりでした。
彼が提案したソリューションは、しばしば見落とされる Module#prepend
を使用するもので、私はそれが十分にクールであり、記事化する価値があると考えました。
そのラッパーでは、メソッドの本体が実行される前に 1 つの「実行中」メッセージが発射され、メソッドの実行自体、そしてメソッドの実行が正常に完了すると最後の「終了」メッセージが発射されるようにします。
Module#prepend
をどのように使用するかを理解するために、まずRubyのメソッド継承がどのように機能するかをよく理解する必要があります。
祖先チェーン
各Rubyオブジェクトは、祖先チェーンと呼ばれるものの末端に位置することになります。 これは、そのオブジェクトが継承する祖先オブジェクトのリストです。 Ruby では、オブジェクトがメッセージを受け取ったときに、どのバージョンのメソッドが実行されるかを決定するために、この Ancestor Chain を使用します。
実際に Module#ancestors
を呼び出すと、任意のオブジェクトの祖先ツリーを表示することができます。 たとえば、以下は Messenger
クラスの祖先チェーンです。
モジュールをクラスに #include
または #prepend
したとき、Ruby はそのクラスの祖先チェーンに変更を加え、どのメソッドをどの順序で発見するかを調整します。 #include
と #prepend
の大きな違いは、その変更が行われる場所です。
から (現在は存在しない) メソッドをインポートするために Module#include
を使用すると、Ruby はそのモジュールを Messenger
クラスの祖先として押し込んできます。
Messenger
でメソッドを呼び出すと、Ruby は Messenger
クラスで定義されているものを調べ、呼び出しに一致するメソッドが見つからない場合は Auditable
まで祖先の鎖を上がって再び探します。 Auditable
で見つからなければ Object
といった具合です。
これが #include
ですね。 もし代わりに Module#prepend
を使ってモジュールの内容をインポートすると、まったく異なる効果が得られます:
#prepend
は Messenger
を Auditable
の祖先にしています。 待てよ。 何?
これは、Messenger
が Auditable
のスーパークラスになったように見えるかもしれませんが、ここで起こっていることは正確ではありません。 Messenger
クラスのインスタンスは Messenger
クラスのインスタンスのままですが、Ruby は Messenger
で使用するメソッドを Auditable
モジュールで探す前に Messenger
クラス自体で探します。
そして、この監査役を作るために利用するのは Auditable#share
というメソッドを作成すると Ruby は Messenger#share
を見つける前にそれを見つけるということです。 そして、Auditable#share
の super
呼び出しを使用して、Messenger
で定義された元のメソッドにアクセス (そして実行!) することができます。
モジュール自身
実際には Auditable#share
というメソッドを作成するつもりはありません。 なぜそうしないのでしょうか。 なぜなら、これを柔軟なユーティリティにしたいからです。 もし Auditable#share
というメソッドをハードコードすると、#share
メソッドを実装したメソッドでのみ使用することができるようになります。 さらに悪いことに、私たちが監査したいすべてのメソッドに対して、 この同じ監査パターンを再実装しなければならないでしょう。
そこで、代わりに、メソッドを動的に定義し、それを起動する audit_method
クラス メソッドを定義することにしました。 この例では、Auditable#share
というメソッドが作成されます。 前述のように、Ruby は Messenger
の元のメソッドを見つける前にこのメソッドを見つけます。
つまり、スーパーコールを使って祖先の連鎖をたどり、Messenger#send
を実行することができるのです。
一度オリジナルのメソッドを呼び出したら、終了メッセージを表示して終了とします。 よくやった、ギャング!
Bringing it all Together
あとは、このモジュールを Messenger
クラスに prepend
追加するだけで、うまくいくはずです:
そしてなんと、それは動作します。 オブジェクト自体を変更することなく、オブジェクトの動作を変更するためにプリペンドを使用することができます。 これはRubyで高次のコンポーネントを作成するための適応的で明確な方法である。 パフォーマンステスト、エラー処理。
まとめ
Rubyのモジュールは多くの人が思っている以上に複雑です。 そして、その違いを理解することで、ユーティリティベルトのための本当にすてきなツールのロックを解除します。
今日、私が Module#include
と Module#prepend
についてだけ話し、 Module#extend
には触れなかったことに気づいたかもしれません。 それは、Module#extend
の動作が同系統のものと大きく異なるからです。
今のところ、もっと学びたいなら、Ruby モジュールを読むことをお勧めします。 Include vs Prepend vs Extend (Léonard Hetsch 著) を読むことをお勧めします。 このすべてをまとめるのに本当に役に立ちました。