Direct Proxiesでmethod missing的なことをやる

  • 43
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Direct Proxiesとは

Direct ProxiesってのはES6の機能の一つで、オブジェクトをラップして様々なタイミングで任意の処理を差し込めるというとっても夢広がリングな機能。

harmony:direct_proxies [ES Wiki]

例えば、プロパティのget/set時に処理を差し込むのはこんな感じ。

(Direct Proxiesは現状だとFirefoxでしか動かない。そして下記のコードは無駄にテンプレートリテラル使ってるのでFirefox 34以上じゃないと動かない)

// Proxyを使って処理を書き換える対象のオブジェクト
var target = { foo: 'bar' };

// Proxyを使ってtargetに対してhandlerを適用したproxyオブジェクトを得る
var proxy = new Proxy(target, {
  // プロパティ読み取り時に処理を実行される処理
  get(target, name) {
    if (name in target) {
      // 存在しているプロパティならそのまま返す(プロパティ読み取りのデフォルトの動作)
      return target[name];
    }
    else {
      // 存在していないプロパティの場合は適当なメッセージを返す
      return `${name}は定義されてないよ!`;
    }
  },

  // プロパティ書き込み時に処理を実行される処理
  set(target, name, val) {
    if (typeof val === 'string') {
      // setしようとしている値が文字列であればそのまま保存する(プロパティ書き込みのデフォルトの動作)
      target[name] = val;
    }
    else {
      // 文字列以外の場合はメッセージを出力して何もしない
      console.log('文字列しか受け付けないよ!');
    }
  },
});

console.log(proxy.foo); //=> bar
console.log(proxy.hoge); //=> hogeは定義されてないよ!

proxy.hoge = 1; //=> 文字列しか受け付けないよ!
console.log(proxy.hoge); //=> hogeは定義されてないよ!

proxy.hoge = 'fuga';
console.log(proxy.hoge); //=> fuga

Proxyの第一引数に対象のオブジェクト(target)、第二引数に差し込む処理(handler)を指定する。このとき、target自身にフックを仕込むんじゃなくて、新たな代理オブジェクト(proxy)を作る。この点についてはObject.observeとかと挙動が違うので注意。

get/set以外にもめっちゃいっぱい処理が差し込めるポイントは用意されているので詳しくはwikiを参照のこと。

method missingとは

このDirect Proxiesが使えるように色々やれることが増えるんだけど、例えばRubyのmethod_missingみたいなことができるようになる。(ちなみにFirefoxには__noSuchMethod__という同じような機能があるんだけど非標準である)

method_missingってのは、classにmethod_missingっていう名前のメソッドを定義しておくと、オブジェクトに対して存在しないメソッドを呼び出した時、NoMethodErrorにならずにmethod_missingが呼ばれるというものだ。

class Foo
  def method_missing(method_name, *args)
    puts "#{method_name}は存在しないよ!"
  end
end

Foo.new.hoge #=> hogeは存在しないよ!

Rubyのmethod_missingは、例えばRailsのfind_by_xxxみたいやつで使われている。DBのカラムにnameageがあったら、RailsのModelは次のようなメソッドを利用できるようになる。

User.find_by_name 'foo'
User.find_by_age 30

この機能はmethod_missingを使って実装されている。

今回はこの機能をDirect Proxiesを使ってBackboneに実装してみる。

ちなみにRailsのこの機能はRails4でdeprecatedになっていて、個人的にも動的にメソッドを呼び出すというのはちょっとアレな感じするのであまり推奨はしないけど、Direct Proxies使うとこんなことができるっていう例としてお楽しみいただければと。

Backboneに動的なfindメソッドを実装する

まずはmethod_missing的なのを実装する。

var handler = {
  get(target, name) {
    // 存在しているプロパティならそのまま返す
    if (name in target) {
      return target[name];
    }

    // methodMissing関数が実装されていれば呼び出す
    if (typeof target.methodMissing === 'function') {
      return target.methodMissing(name);
    }
  }
};

これをhandlerに指定して、methodMissing関数を実装したオブジェクトをtargetとしてProxyを呼び出せばいい。

var obj = new Proxy({
  methodMissing(name) {
    return () => `${name}は存在しませんよ!`;
  }
}, handler);

console.log(obj.foo()); //=> "fooは存在しませんよ!"

ちなみにこのmethodMissing関数は、メソッドがなかったときに呼び出されるんではなく、プロパティがない場合に呼び出されるので正確にはmethodMissingというpropertyMissingのほうが正しいが今回は気にしないでおこう。そしてプロパティアクセス時なので、method_missing的な動作にするために、methodMissingでは関数を返す必要がある。

これをBackbone.Collectionに適用すると次のようになる。

var CollectionBase = Backbone.Collection.extend({
  constructor() {
    Backbone.Collection.apply(this, arguments);

    return new Proxy(this, handler);
  },
  methodMissing(name) {
    var matched = name.match(/^findBy([A-Z][0-9A-Za-z]+)$/);
    if (!matched) return;

    var attrName = matched[1].charAt(0).toLowerCase() + matched[1].slice(1);
    if (!(attrName in this.model.prototype.defaults)) return;

    return (val) => this.findWhere({ [attrName]: val });
  }
});

methodMissingではメソッド名をチェックしてfindByXxxっていうメソッドかつ、Xxxの部分がModelのdefaultsに存在しているattributeだったら検索メソッドを実行する関数を返すようにしている。

そしたら上記のCollectionを継承して適当なクラスをつくる。

var MyModel = Backbone.Model.extend({
  defaults: {
    name: null,
    age: null,
  }
});

var MyCollection = CollectionBase.extend({ model: MyModel });

そうするとMyCollectionfindByNamefindByAgeというメソッドが利用できるようになる。

var coll = new MyCollection([
  { name: 'foo', age: 20 },
  { name: 'bar', age: 30 },
  { name: 'baz', age: 40 },
]);

console.log(coll.findByName('bar').get('age')); //=> 30
console.log(coll.findByAge(20).get('name')); //=> foo

また、Modelのdefaultsに存在しないattributeの名前で呼び出したらちゃんとエラーになる。

coll.findByFoo(); // => TypeError: coll.findByFoo is not a function

Direct Proxiesが使えるようになると、他にも色んなことができるようになると思う。この前のES6+カジュアルトークで発表されていた、immutableなArrayを作るっていうのも面白かった。

ECMAScript6による関数型プログラミング

この投稿は JavaScript Advent Calendar 20143日目の記事です。