TypeScript/ES7でprivate風メソッド、プロパティ

ガチガチに固めた、静的な言語のようなクラスをJavaScript(TypeScript+ES6+ES7)で作ってみるシリーズ。

# private風なクラスメンバー（メソッド、プロパティ）

ES6ではクラスが取り入れられたがアクセス修飾子がないためにカプセル化がやりにくい。TypeScriptにはアクセス修飾子があるが、コンパイル時のチェックであって実行時には簡単にアクセスできてしまう。

```javascript:お願いですアクセスしないでくだせぇ
class A{
	private p = 1;
}
var a = new A();
a["p"];			//ヒャッハー！アクセスできるぜー！
(a as any).p;	//コンパイルエラー？知らないなァー！
```

そこでES7の`metadata`と`decorator`を組み合わせて、外部からのアクセスにたいして`Error`を吐くようにして擬似的なprivateを再現してみることにする。

# metadata + decorator

やり方としては次の通り。

 1. アクセスを許可する同じクラスのメンバーに付加情報を加えておく
 2. アクセスされたときにアクセス元が付加情報をもっていれば許可、もっていなければ不許可（エラー)

## metadata

ES7で[decoratorと一緒に提案されているもの](https://github.com/jonathandturner/decorators/blob/master/about-metadata.md)で、クラスだったりメソッドだったりプロパティだったりにメタな情報を追加するもの。実は既にTypeScriptでは1.5.3から`--emitDecoratorMetadata`オプションを指定する事で実行時に型情報を取り出せたりする。


metadataは`Reflect`に追加されるAPIで操作する。`Reflect`自体の対応環境が多くないが、[polyfilがある](https://github.com/rbuckton/ReflectDecorators)のでnpmでインストールしておく。

```
npm i reflect-metadata --save
```

## decorator

アクセス演算子風に指定できるようにするには、このシリーズでおなじみのdecoratorを使う。`private`は予約語なので使えないので`privated`という名前にしてみる。

```javascript
class A{
	@privated private prop;
	@privated private method(){};
}
```

似たような単語が２回続くが、コンパイル時・ランタイム時両方対応のためには仕方ない。

## metadataをつける

`@privated`デコレータの本体部分で、デコレートされたプロパティ/メソッドと同じクラスメンバー全てにmetadetaを付与してゆく。今回は適当に`custom:private:【クラス名】`というキーにしてみた。metadataの値にはクラスコンストラクタの参照を入れる。

```javascript
export function privated<T>(target:T, key:string|symbol, desc?:PropertyDescriptor):any{
	
	//キー名
	const pName = "custom:private:"+target.constructor.name;
	
	//同じクラスのメンバー全部に付与する
	const protoNames = Object.getOwnPropertyNames(target);
	const protoLen = protoNames.length;
	for(let i=0|0;(i|0)<(protoLen|0);i++){
		//省略...
		Reflect.defineMetadata(
			pName,
			target.constructor,
			o	//対象のメンバー
		);
	}
	
	//...
}
```


なお、最初の定義時にあるもののみに付与されるので、インスタンスに後から追加されたものにはつかないが、静的な言語風クラスを作る目的からすれば問題ない。

## アクセス元の判定

無事metadataを付与できたら、アクセスされた時にそのmetadataを持っているかどうかで判定するよう処理を加える。

処理を加えるのもデコレータで。クラスプロパティには全てgetter/setterを設定することで、アクセス時に処理が加えられるようにすることがポイント。

```javascript:処理を加える
	if(desc){
	//class method
		return <PropertyDescriptor>{
			enumerable:false,	//for...inで見えないようにしておく
			configurable:desc.configurable,
			value:pFunc	//処理を加えて置き換える
		};
	}else{
	//class property
		Object.defineProperty(
			target,
			key,
			{
				set:pSet,	//処理を加える
				get:pGet,	//処理を加える
				enumerable:false,
				configurable:true
			}
		);
	}
```

なお、metadataがあるかどうかだけの判定だけでは、同じようなmetadataを追加すれば外部からもアクセスできてしまう。そこで、`function.caller`を使って呼び出し元が同じクラスメンバーかどうかも判定する。

```javascript:メタデータとメンバーかどうかで判定
const pFunc = function(...args:any[]){
	const capsuled = Reflect.hasMetadata(pName,pFunc.caller) && Reflect.getMetadata(pName,pFunc.caller)===target.constructor;
		
	if(capsuled && isMember(pFunc.caller,null,target)){
		//内部からのアクセスには元のメソッドの内容を実行
		desc.value.apply(this,args);	
	}
	else{
		//外部からのアクセスはErrorをthrowする
		throw Error(`Cannot call @privated method "${key}"`);
	}
}
```

# private風に

githubに今回つくった[`@privated`デコレータ](https://github.com/ConquestArrow/deco/blob/master/src/private.ts)とその[サンプルテストコード](https://github.com/ConquestArrow/deco/blob/master/test/private-test.ts)を置いてある。

一応、以下のテストコードに書かれている範囲では動作している。

 * [ブラケットアクセス](https://github.com/ConquestArrow/deco/blob/master/test/private-test.ts#L104)
 * [Object.getOwnPropertyDescriptor().value](https://github.com/ConquestArrow/deco/blob/master/test/private-test.ts#L118)
 * [インスタンスに生やしたメソッドからアクセス](https://github.com/ConquestArrow/deco/blob/master/test/private-test.ts#L127)
 * [metadataを追加](https://github.com/ConquestArrow/deco/blob/master/test/private-test.ts#L147)
 * [プロパティとして後から追加＆metadata付与](https://github.com/ConquestArrow/deco/blob/master/test/private-test.ts#L165)
 * [サブクラスのメソッド経由のアクセス](https://github.com/ConquestArrow/deco/blob/master/test/private-test.ts#L375)

が、どこかに抜け道があるかもしれないので"private風"、ということで。

メタデータ付与と判定処理でクラスメンバーの走査をしているのでパフォーマンス面では実用にならないかもしれない（未確認）。