はじめに
エンジニアになってからフレームワークはRails一筋で通していましたが、最近 nodeに魂を売り始めた @alfaです。
しかし、Javascriptを書いていて一つ気になることがあるのです。
Rubyをこよなく愛すエンジニアの皆さんは、感じたことがあるのではないでしょうか?そう「DSLを気軽に定義できない」んです!!
今回はJavascriptでどうしてもDSLを作りたかったお話です。
一体何がやりたいのか
Javascriptのgetter/setterの定義、めっちゃ面倒ですよね?
class Hoge {
get hoge() {
return this._hoge;
}
set hoge(v) {
this._hoge = v;
}
get fuga() {
return this._fuga;
}
set fuga(v) {
this._fuga = v;
}
}
例えば Ruby で言う attr_acessor
を作りたい!!
class Hoge {
attrAcessor('hoge');
attrAcessor('fuga');
}
3秒後に反射的に書いたコード
class AttributeAccessor {
attrAcessor(name) {
Object.defineProperty(this, name, {
get: function() { return this[`_${name}`] },
set: function(v) { this[`_${name}`] = v },
});
}
}
あとは、extend…、いや、ん?…
そもそも言語仕様的にRubyのようなDSLは作れないんですね。
でもRubyみたいに手軽にDSLを実装したい!
妥協案 (不採用)
関数内にDSLを書く感じに作ればいいか
class Base {
constructor() {
this.constructor.definer(this);
}
attrAcessor(name) {
// 略
}
}
class Hoge extends Base {
static definer(d) {
d.attrAccessor('hoge');
}
}
と妥協したものの...
class Hoge extends Base {
//↓これ嫌い
static definer(d) {
d.attrAccessor('hoge');
//↑これ嫌い
}
}
なんだかイマイチでした。
和解案
最終的に下記のような形で和解しました。
class Hoge extends Base {
static definer() {
attrAccessor('hoge');
}
}
さて、みなさんは上記のDSLをどのように実現したか想像がつくでしょうか?
ちなみに、下記のコードでは実現できません。
class Hoge extends Base {
constructor() {
this.constructor.definer.bind(this)();
}
}
なぜなら attrAccessor
の前に this
がないからです。
実現方法
eval
と契約してDSLおじさんになってよ
const scripts = [];
scripts.push(`const attrAcessor = this.attrAcessor`);
scripts.push(this._getFunctionBody(this.constructor.definer));
eval(scripts.join(';'));
- DSLの命令を定義
- 定義(definer)関数の内部を抽出
-
1.
2.
を結合しeval
で評価
(eval
の直接的呼び出しはローカルスコープになるため this.attrAcessor
の参照が可能です。)
※ あくまでも概念の説明用です、thisを束縛していないため動作しません。
※ 実際の実装はこちら
命令の定義までevalしている理由
ローカルスコープが参照できるなら、命令の定義まで eval
する必要はないと考えるかもしれません。
const attrAcessor = this.attrAcessor;
eval(this._getFunctionBody(this.constructor.definer));
しかし、上記のコードでは最適化やminifyされた場合に、変数名が圧縮されたり、そもそも削られてしまうかもしれません。(と言うかされます)
その対策として、命令の定義も eval
しています。
一応ライブラリにしています
需要あるかわかりませんが、自分で使うので… track-dsl
こんな感じで使用します。
const DSL = require('track-dsl');
class Base {
constructor() {
const dsl = new DSL(this, {
'attrAccessor': {func: this._attrAcessor, binding: this},
});
dsl.evaluate(this.constructor.definer);
}
_attrAcessor(name) {
Object.defineProperty(this, name, {
get: function() { return this[`_${name}`] },
set: function(v) { this[`_${name}`] = v },
});
}
}
class Hoge extends Base {
static definer() {
attrAcessor('hoge');
attrAcessor('fuga');
}
}
hoge = new Hoge();
hoge.hoge
hoge.fuga
以上です。
もっと良い方法の提案、ライブラリへのPRお待ちしています!!!