What is the difference between proxy and reify?

まえがき

隣の若者にいきなり「 proxyreify の違いが分からない」と言われたのが数日前のお昼の出来事。

ドキュメントを読めばある程度分かると思うのだけれど、どちらも Java のインターフェイスを実装した匿名クラスのインスタンスを 生成することができるというところで役割が似ているように思えたんだと思う。

proxy と reify ってなんでしたっけ

おおまかには公式のリファレンスに書いてあるので是非読んでもらいたいのだけれども、それで終わってしまっては面白くないのでそれぞれどのようなことを実現できて、どのようなときに利用するのが良いのか説明していこうと思う。

せっかちな人の為に、簡単に書いてしまうと次のような違いがある。

  • proxy は任意のクラスを継承 and/or ひとつ以上のインターフェイスを実装した匿名クラスのインスタンスを作成できる。
  • reify はひとつ以上のプロトコルまたはインターフェイスを実装した匿名クラスのインスタンスを作成できる。

proxy が使えるときと使い方

proxy という名前が示す通り、 Java のクラスに対してプロキシクラスを作るものだと覚えましょう。

proxyJava 由来のクラスを継承 したいとき、または インターフェイスを実装 したいときに使えます。ちなみに継承の制約は Java と同じなので final なクラスを継承することは できません 。 また、クラスを継承するのと同時に任意個のインターフェイスを指定して実装することが出来ます。ただし、任意のクラスを継承せずインターフェイスのみを指定することも可能です。 クラスを指定せずにインターフェイスのみを実装する場合は、 Object クラスを設定されたと見做し Object クラスを継承します。

簡単に例を示しましょう。

package demo.example;

public class Greeting {
    public final String name;

    public Greeting (String name){
        this.name = name;
    }

    public String hello (){
        return "Hello, " + this.name;
    }
}

このような Java のクラスがあるときに、これを継承して実装を変更してみましょう。 まず、これを Clojure から利用するとこのようになります。

(import demo.example.Greeting)

(.hello (Greeting. "ayato_p"))
;; "Hello, ayato_p"

ここで Hello, ayato_p と表示されていたものを Hi, ayato_p としたい場合、 proxy を利用して次のように記述できます。

(.hello
 (proxy [Greeting] ["ayato-p"]
   (hello [] (str "Hi, "(.name this)))))
;; "Hi, ayato-p"

proxy マクロの第一引数はクラスとインターフェイスのベクタを取るようになっているので、今回は [Greeting] としてます。 第二引数はスーパークラスのコンストラクタに渡す引数です(引数 0 のコンストラクタがあるなら空でよい)。 第三引数以降は実装する関数を書いていきます(気をつけないといけないのはシグネチャは Java の実装の方に合わせないといけないということです hello メソッドをプロキシするので Greeting クラスの hello メソッドと同様のシグネチャにします)。

また、この例では this を使っていますが、これは暗黙的に proxy マクロが関数の第一引数として渡してくれているものです(ドキュメントにも記述があるので確認してください)。 マクロ展開するとどうなっているか分かります。

(let [p__5959__auto__ (new
                        demo.core.proxy$demo.example.Greeting$ff19274a
                        "ayato-p")]
  (init-proxy
    p__5959__auto__
    {"hello" (fn ([this] (str "Hi, " (.name this))))}) ;; 暗黙的に this を第一引数にとる関数へと変換される
  p__5959__auto__)

次に単純な Hi, ayato_p!! と単純にエクスクラメーションマークを付け足したいときを考えます。

(.hello
 (proxy [Greeting] ["ayato-p"]
   (hello [] (str (proxy-super hello) "!!"))))
;; "Hello, ayato-p!!"

proxy-super を呼び出すことで親クラス( Greeting )の元々の hello メソッドを呼び出すことができました。

ここまでがおおよそ、 proxy の使い方になります。また proxy マクロで生成された匿名クラスのインスタンスは元のクラスと継承関係があります。

(ancestors
 (class
  (proxy [Greeting] ["ayato-p"]
    (hello [] (str "Hi, "(.name this))))))
;; #{clojure.lang.IProxy demo.example.Greeting java.lang.Object}

なので、次のようなことが可能になります。

(defprotocol IByeBye
  (bye-bye [this]))

(extend-protocol IByeBye
  Object
  (bye-bye [this]
    "Bye-bye, anonymous")

  Greeting
  (bye-bye [this]
    (str "Bye-bye, " (.name this))))

(bye-bye (Object.))
;; "Bye-bye, anonymous"

(bye-bye (Greeting. "ayato_p"))
;; "Bye-bye, ayato_p"

(bye-bye (proxy [Greeting] ["ayato_p"]))
;; "Bye-bye, ayato_p"

継承関係があるということで mutlimethod やこのような protocol での拡張も、親クラスのインスタンスと同じように利用できるのです。

という風にここまでで proxy マクロの使い方を説明しました。 Clojure だけで完結できる場合はほとんど使うことはないと思いますが、もしも困ったら proxy を思い出してください。

reify が使えるときと使い方

reify という名前の通りインターフェイスやプロトコルを具象化するための仕組みです。

reifyJava 由来のインターフェイス または プロトコルをひとつ以上実装 したいときに使えます。また、特別にプロトコルとインターフェイス以外でも Object クラスだけは指定することができます。

簡単に例を示します。

(def r
  (reify
    IByeBye
    (bye-bye [this] "Bye Bye 👋")

    clojure.lang.IFn
    (invoke [this] "INVOKED!!")

    Object
    (toString [this] "reified")))

(bye-bye r)
;; "Bye Bye 👋"

(r)
;; "INVOKED!!"

(str r)
;; "reified"

proxy の例で登場した IByeBye プロトコルと clojure.lang.IFn インターフェイスを実装して、 Object/toString を上書きしてみました。

このように適当なプロトコルやインターフェイスを実装したものとして扱うことが出来ます。この説明だけで使いどころが分かる人の方が少ないと思いますが、 reify を使うメリットは名前付けを省略できること、一度しか作る必要がない型のオブジェクトを生成できること、レキシカルスコープを参照したメソッドのボディを記述できることです。

(def r
  (let [v 10]
    (reify clojure.lang.IFn
      (invoke [this] v))))

(r)
;; 10

この例のようにメソッドのボディ部で外側の v をクロージャとして閉じ込めることが出きるのです(便利!)。

それから Stuart Sierra’s Component を利用している場合、プロトコルを実装したコンポーネントがあると思います。

(defprotocol IMailer
  (send-mail [mailer title content]))

(defrecord Mailer [endpoint]
  IMailer
  (send-mail [_ title content]
    ;; ...
    ))

このようなプロジェクトでテストを書くときに、上記のコンポーネントをそのまま使うとリアルな API を叩いたりしてしまって困ってしまいます。

なので、次のようにモックオブジェクト生成するコンストラクタ関数を作って実際のコンポーネントの代わりに挿入したりします。

(defn new-mailer-mock [endpoint]
  (reify IMailer
    (send-mail [_ title content]
      ;; ...
      )))

proxy と reify の使い分け方

基本的に達成できるコトが違うふたつのマクロですが、微妙に似ているためどちらを使えば悩むときがあると思います。そういうときは reify のドキュメントにある proxy との違いを参考にすると良いです。

書いてあるのは

  • reify はプロトコルもしくはインターフェイスのみサポートしていて、具体的な親クラスには利用できない
  • メソッド本体は外部クラスではなく、生成されたクラスのメソッドである
  • インスタンスメソッドの実行はマップのルックアップを行わず、直接呼び出される
  • メソッドマップによる動的なメソッドの入れ替えをサポートしていない

つまり、 proxyreify のどちらも使える状況であれば reify を選ぶ方がパフォーマンス的には良さそうです。

まとめ

proxyreify の違いをしっかり把握して活用していきましょう。