※ このエントリは策定中の仕様やfix前の実装に従って書いています(2015.04.07). 仕様の状況によっては, 記載内容に不整合が出る可能性があります.
Decoratorの基本
TypeScriptの1.5.0-alphaから Decoratorが利用可能になった.
Decoratorは @Component
のように記述して付加情報をclassやmethod等に付与するための仕組みで, ES7の仕様として検討されている(2015.04.07時点でstage1).
TypeScript 1.5.0-alphaでは, Decoratorを付与できる箇所は下記である.
- class 宣言部
- classのプロパティ宣言部
- classのメソッド宣言部
- メソッドの引数
@someDecorator
class MyClass {
@someDecorator
message: string;
constructor(@someDecorator private name: string){}
@someDecorator
greeting(): void {}
@someDecorator
static sayHello(): void {}
}
ES7のproposal では, Objectリテラル中のmethod, property accessorにも付与できるとのことだが, TypeScript, Babel共に現状未対応の模様.
逆に下記のような場所にはDecoratorは付与できない.
- module
- constructor
- 関数(
let someFunc = function(){...}
)
Decoratorは関数として実装する. Decoratorのsignatureは付与対象に応じて異なる.
先に列挙した付与対象に応じて, それぞれ, ClassDecorator
, PropertyDecorator
, MethodDecorator
, ParameterDecorator
となる.
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Function, propertyKey: string | symbol, parameterIndex: number) => void;
MethodDecoratorの引数に出現する TypedPropertyDescriptor
はES5から導入されたPropertyDescriptorに対応している(MDN: PropertyDescriptorの解説)
tsc
で-t es5
を付けずにDecoratorを含んだコードをコンパイルすると怒られるのは, ES3にはPropertyDescriptorが存在しないからだ.
desugar
さて, Decoratorはcompile後にes5コードにdesugarされる. 一番簡単なClassDecoratorを例にすると, 変換イメージは下記のようになる.
@someClassDecorator
class MyClass {
// ...
}
var MyClass = (function {
function MyClass() {
// ...
}
return someClassDecorator(MyClass);
})()
Decoratorにはコンストラクタが引き渡されるため, prototypeを弄ってしまえば, 元々のclassには存在しないプロパティを追加することもできる. "Decorator" と呼ばれる所以だ.
let someClassDecorator = (clazz: Function) => {
clazz.prototype.hogehoge = 'FOOFOO';
return clazz;
};
上述のsignatureの通り, MethodDecoratorも PropertyDecorator型の戻り値を返すことができ、PropertyDecorator#valueの値を変更することで, 本来のメソッドの挙動を変更することができる(コーディング例は後述).
引数付きのDecorator
Decorator利用時に任意の引数を渡すこともできる.
引数付きのDecoratorを作成する場合, 先述したdecoratorのsignatureを持つfunctionを返却する関数を用意すればよい.
let someClassDecorator = (/* Decoratorの引数 */ option?: string) => {
return <TFunc extends Function> (target: TFunc) => {
// ...
};
};
@someClassDecorator('オプション')
class MyClass {}
何をDecoratorとして括りだすべきか.
さて, Decoratorはどのような場合に用いるべきだろうか?
先述の通り, Decoratorを使えば, classのprototypeやmethod本体に侵襲できるため, その気になれば何でもできる.
もともとのclass定義をガン無視して、全く関係ないコンストラクタを返すことだってできるが, そんなことしても誰の特にもならない.
僕は元々エンプラ系のJavaをやっていた時期があり, どうしてもJavaのAnnotationとDecoratorとの比較になってしまうが, JavaにおけるAnnotationの利用例を考えると, 頻出するパターンは下記と思う.
AOP
Aspect(コンポーネントの主処理と直交する関心事)を処理させるためにDecoratorを作成するケース.
良く例に挙がるテーマはロギングや異常系ハンドリング(例外処理), トランザクション境界の宣言, 透過参照関数のcache, memoize等.
フレームワークとの協調動作
例えば, DIコンテナへComponent情報を登録するようなパターン.
AngularJS 2 quickstartにて, @Component
や @Template
というDecoratorが出てくるが, このパターンの典型例である.
いずれにせよ、副次的な処理と主処理を分けて, 副次的処理をDecoratorの裏に隠蔽化することが目的である.
まぁどこまでが「副次的」なのかは、考え方次第な気もするが...
(例えば, JavaのJAX-Bなんて只のJava Beanに対して大量のAnnotationを付与するため, Annotationである必要性が全く分からなかった)
JavaScript, TypeScriptならではのDecorator活用パターン?
僕の貧弱な想像力では Java とのアナロジしか思い浮かばなかったが, JavaScriptやTypeScriptならではなDecorator利用パターンはないのかしら...
何か面白い活用方法を知っている・思いついた等があれば, 教えてください.
Case Study
ここからは, 実際のコード例で話を進めていく.
Case 1. AOP
以下はメソッド実行時に引数と戻り値の情報をコンソールに吐き出すMethodDecoratorのサンプル.
ロギングを主処理から分離した例である.
let debug = (target: any, name: string, descriptor: PropertyDescriptor) => {
let delegate = descriptor.value;
descriptor.value = function () {
let args: string[] = [];
for (let i = 0; i < arguments.length; i++){
args.push(arguments[i]);
}
console.log(`${name} in: ${args.join()}`);
let result = delegate.apply(this, arguments);
console.log(`${name} out: ${result}`);
return result;
};
return descriptor;
};
class Calc {
@debug
add (a: number, b: number): number{
return a + b;
}
}
new Calc().add(1, 4);
tsc -t es5 loggingSample.ts; node loggingSample.js
add in: 1,4
add out: 5
Case 2. xUnit的な何か
Decoratorを使ったテスティングFWの例である.
(MochaやChaiに習って、sayu(白湯)という名前にしてみた. 只の言葉遊び)
// テスティングフレームワーク相当. 白湯.
module sayu {
export function test (target: any, name: string, descriptor: PropertyDescriptor): void {
let val = descriptor.value;
val.__test = true;
}
export function suite (target: any): void {
let testSuite = new target();
let successCount = 0;
let failCount = 0;
for ( let key in target.prototype) {
if(target.prototype[key].__test) {
console.log(`Start test case ${key}`);
try {
testSuite[key]();
successCount++;
console.log(' OK')
} catch (e) {
failCount++;
console.error(` Fail to assert. ${e.message}`);
}
}
}
console.log(`${successCount} cases succeed, ${failCount} cases fail`);
}
export module assert {
export function assertEquals <T> (expected: T, actual: T) {
if (expected !== actual) {
throw new Error(`Assertion fail. Expected: ${expected}, but Actual: ${actual}.`)
}
}
}
}
// 利用側
import suite = sayu.suite;
import test = sayu.test;
import assertEquals = sayu.assert.assertEquals;
let fizzBuzz = function (n: number): string {
if(n % 15 === 0) {
return 'FizzBuzz';
}else if(n % 5 === 0) {
return 'Buzz';
}else if(n % 3 === 0) {
return 'Fizz';
}else {
return n + '';
}
}
@suite
class fizzbuzzTest {
@test
入力が1() {
assertEquals('1', fizzBuzz(1));
}
@test
入力が3() {
assertEquals('Fizz', fizzBuzz(3));
}
@test
入力が5() {
// Fail to assert
assertEquals('5', fizzBuzz(5));
}
}
classに付与するDecoratorを作り, その内部でテストメソッドを呼び出すように実装している.
まるで JUnit 4.x みたいだ(それが良いことなのかどうかはさておき).
上記のコードを実行すると, ↓のようになる.
tsc -t es5 testFwSample.ts; node testFwSample.js
Start test case 入力が1
OK
Start test case 入力が3
OK
Start test case 入力が5
Fail to assert. Assertion fail. Expected: 5, but Actual: Buzz.
2 cases succeed, 1 cases fail
Case 3. AngularJS
最後にもう少し実用的な例を一つ. @vvakame氏のgist が元ネタ.
AngularJSにDecoratorを導入する例だ.
AngularJSのControllerやServiceのコンポーネントは, コンストラクタ関数をモジュールに登録させるため, TypeScriptのclassと相性がよい.
また, Angular 2.xでは, さらにClassによるコンポーネント指向, Decoratorとの連携が進む見込みなので, 今のうちから慣れておく意味もこめて、Angular 1.xでもDecoratorを使ってみる.
まずはDecoratorを使わずに, TypeScriptでAngularJS 1.x のコードを書いてみる.
/// <reference path="../typings/angularjs/angular.d.ts" />
interface NgMyApp extends ng.IModule {}
let ngMyApp: NgMyApp = angular.module('ngMyApp', []);
class MainCtrl {
message: string;
constructor() {
this.message = 'Hello';
}
}
// モジュールへの登録
ngMyApp.controller('MainCtrl', [MainCtrl]);
htmlテンプレート上からは, ng-controller="MainCtrl as main"
のようにcontrollerAs Syntaxを利用することで, {{main.message}}
のようにMainCtrlのpropertyを参照できる.
このままでも十分使えるのだが, 少し鬱陶しいのが, コード末尾で登場するngMyApp.controller(...)
の部分.
この部分にDecoratorを使うと少しすっきりする.
/// <reference path="../typings/angularjs/angular.d.ts" />
interface ModuleOption {
moduleName: string;
}
interface NamedFunction extends Function {
name: string;
}
let controller = (opt: ModuleOption) => {
return (controllerClazz: Function) => {
let clazz = <NamedFunction>controllerClazz;
angular.module(opt.moduleName).controller(opt.componentName || clazz.name, clazz);
};
};
interface NgMyApp extends ng.IModule {}
let ngMyApp: NgMyApp = angular.module('ngMyApp', []);
@controller({moduleName: 'ngMyApp'})
class MainCtrl {
message: string;
constructor() {
this.message = 'Hello';
}
}
@controller
Decoratorを用意し, Decorator内部にmodule登録処理を共通化する.
Decorator定義の分, 元々のコードと比較して量が増えてはいるものの, Controllerが大量になっても, @controller
を付与すれば済むようになった.
このままだとminifyかけたときにinjector.annotate
が空振りする可能性があるので, $inject
を明示するためのDecoratorも用意してみた.
また, Serviceも同様にDecoratorを用意すると下記のようになる.
/// <reference path="../typings/angularjs/angular.d.ts" />
// Decorators.
interface ModuleOption {
moduleName: string;
componentName?: string;
}
interface NamedFunction extends Function {
name: string;
}
let inject = (injectableNames: string[]) => {
return (target: Function) => {
target.$inject = injectableNames;
};
};
let controller = (opt: ModuleOption) => {
return (controllerClazz: Function) => {
let clazz = <NamedFunction>controllerClazz;
angular.module(opt.moduleName).controller(opt.componentName || clazz.name, clazz);
};
};
let service = (opt: ModuleOption) => {
return (serviceClazz: Function) => {
let clazz = <NamedFunction>serviceClazz;
angular.module(opt.moduleName).service(opt.componentName || clazz.name, clazz);
}
}
// Application.
interface NgMyApp extends ng.IModule {}
let ngMyApp: NgMyApp = angular.module('ngMyApp', []);
@service({moduleName: 'ngMyApp', componentName: 'greeter'})
class Greeter {
sayHello(): string {
return 'Hello, ng & ts!';
}
}
@controller({moduleName: 'ngMyApp'})
@inject(['$window', 'greeter'])
class MainCtrl {
message: string;
constructor(private $window: ng.IWindowService, greeter: Greeter) {
this.message = greeter.sayHello();
}
}
Angular 1.x のアプリをTypeScriptで開発する機会があればお試しあれ.
Decoratorの動作順序(2015.04.13 追記)
最後のAngularの例では, classに複数のDecoratorを張っていたが, TypeScriptでは下→上の順番で評価される. 先述の例で言えば, inject()
→ controller()
の順番.
そういうもんか, くらいの気持ちでいたが, BabelのDecoratorsを試して見たところ、上→下の動作順序でTypeScriptとは逆だったので要注意な気配がプンプンする(サンプル).
今の段階では, 複数のDecoratorを許容するのなら, 交換可能であるように実装するしかないのかな。
- さらに追記(2015.04.13)
上記の愚痴をTwitterでこぼしてたら, その日のうちにBabelに修正が入った. Babelも下→上の動作順序になりました.
@sebmac さんマジぱねっす(しかも日本語のtweet翻訳して追っかけてるとか凄すぎる).
まとめ
- Decoratorの基本は, classやmethodをラップする関数.
- Decoratorにaspectを実装すればAOPっぽいことが実現できる.
- classやmethod定義の拡張ポイントとして利用することで, Decorate対象をフレームワークに連携可能.
- Decoratorの動作順序には気を使うべし