#あらすじ
getter/setterは面倒なだけでなく、コード可読性を下げる「不」必要悪なので排除してみたお話。
#プロローグ
ES2015でクラス記法が導入されたのはとてもいいことだが、その裏でこっそりと招かれざる客が侵入していたことにお気づきだろうか。そうgetter/setterである。カプセル化はOOPの根幹であり、プロパティへの直接アクセスを避けることは当然である。しかしこんな汚らしい記法は到底許容できない。
class A{
constructor(val){
this._val = val;
}
get val(){
return this._val;
}
set val(val){
this._val = val;
}
}
全くもってJavaScriptらしくない。スマートでちょっと斜に構えた愛すべきJavaScriptの姿はこれではない。冗長なコードは可読性を助く場合のみ可だ。getter/setterがあることでむしろコードはわかりにくくなっている。さあ、汚物は消毒だ。
#理想の世界
理想とするコードはこうだ。
class A{
constructor(readonly , writable){
//先頭に"_"が一つの場合、読込専用(外部アクセス用変数readonlyとgetterは自動的に生成)
this._readonly = readonly;
//先頭に"_"が二つの場合、読み書き可能(外部アクセス用変数writableとgetter/setterは自動的に生成)
this.__writable = writable;
}
}
これなら不要なコード記述による可読性の阻害も発生しない。
#「defineProperty」が鍵
さて汚物消毒用の武器だが実は既に素材は用意されている。Object.defineProperty関数を利用すれば動的にgetter/setterを設定できる。例えばこんな感じだ。
class A{
constructor(val){
this._val = val;
//外部アクセス用変数valの定義
Object.defineProperty(this, "val",
{
get: function () {
return this._val;
}
})
}
}
これで内部変数_valの外部アクセス用変数valを宣言できる。この場合getしか定義していないのでvalは読込専用だ。
しかしこのままでは元よりも可読性が悪くなっているので、もちろんこのままでいいはずはない。
この素材を利用して武器を作るのだ。
#火炎放射器を作る
Object.definePropertyを利用して汚物を一掃する火炎放射器を作ってみた。
var autoGetSet = function(){
//宣言済みの内部変数全体を対象とする
Object.keys(this).forEach(function (prop) {
//内部変数の先頭から"_"を切取って外部アクセス用変数名を定義する
let realprop = prop.replace(/^_+/, '');
//先頭文字のみが"_"である場合、読込専用なのでgetterのみ定義する
if (prop.length > 1 &&
prop[0] === '_' &&
prop[1] !== '_'){
Object.defineProperty(this, realprop, {
get: function () {
return this[prop];
}
})
//先頭から"_"が連続している場合、getterとsetterを定義する
}else if (prop.length > 2 &&
prop[0] === '_' &&
prop[1] === '_'){
Object.defineProperty(this, realprop, {
get: function () {
return this[prop];
},
set: function (val) {
this[prop] = val;
}
})
}
}, this);
}
autoGetSet関数がクラスの外部で定義されている場合の利用方法はこのようになる。
class A{
constructor(readonly , writable){
this._readonly = readonly;
this.__writable = writable;
autoGetSet.bind(this)();
}
}
これでようやくJavaScriptらしくなった。
おかえりJavaScript。
#改良型火炎放射器
通常setterはチェック等のロジックを含むことが多い。しかし現状のautoGetSetではそういった対応ができない。
そこでsetterの前後にインタセプタを設置し、動的に処理を差し込むことができる改良型を作成してみた。
var autoGetSet = function(){
//宣言済みの内部変数全体を対象とする
Object.keys(this).forEach(function (prop) {
//内部変数の先頭から"_var"を切取って外部アクセス用変数名を定義する
let realprop = prop.replace(/^_+/, '');
//先頭文字のみが"_"である場合、読込専用なのでgetterのみ定義する
if (prop.length > 1 &&
prop[0] === '_' &&
prop[1] !== '_'){
Object.defineProperty(this, realprop, {
get: function () {
return this[prop];
}
})
//先頭から"_"が連続している場合、getterとsetterを定義する
}else if (prop.length > 2 &&
prop[0] === '_' &&
prop[1] === '_'){
//値設定前に実行されるインターセプタ関数名: beforeSet外部アクセス用変数名
//値設定後に実行されるインターセプタ関数名: beforeSet外部アクセス用変数名
let beforeInterceptorFuncName = "beforeSet" + realprop;
let afterInterceptorFuncName = "afterSet" + realprop;
Object.defineProperty(this, realprop, {
get: function () {
return this[prop];
},
set: function (val) {let oldval = this[prop];
//インターセプタが登録されている場合セッターの前後でコードを実行する
this[beforeInterceptorFuncName] && this[beforeInterceptorFuncName](val);
this[prop] = val;
this[afterInterceptorFuncName] && this[afterInterceptorFuncName](val);
}
})
}
}, this);
}
class A{
constructor(readonly , writable){
this._readonly = readonly;
this.__writable = writable;
autoGetSet.bind(this)();
//writableにインターセプタを設置する
this.beforeSetwritable = function(val){if(val < 0){throw("0以上の値を設定してください")}};
this.afterSetwritable = function(val){console.log("新しい値は" + val + "です")};
}
}
var a = new A()
a.writable = 100
"コンソール出力: 新しい値は100です"
a. writable = -100
"コンソール出力:Uncaught 0以上の値を設定してください"
#蛇足
autoGetSet内でthis参照しているためbindを利用しているが、これが気に入らない場合autoGetSetに引数を設定して関数内ではthisを利用せず引数参照するようにしても問題ない。またstrictモードではない場合、読込専用プロパティに値を書き込もうとしても「エラーも出ず」「設定もされない」だけだが、strictモードではきっちりエラーとなるので可能な限りstrictモードを利用するのが良いだろう。