ES2015の登場とトランスパイラーの普及により、コードや記事の中で言語の新機能を見かけることが増えました。とりわけ難しい新機能がJavaScriptのデコレーターです。
デコレーターはAngular2に採用されたことで人気が高まりました。デコレーターは現時点ではAngularではTypeScriptで使うことができ、JavaScript(ECMAScript)のプロポーザルではステージ2(編注:ドラフト)です。将来的にはJavaScriptにも採用されるでしょう。そこで、デコレーターの役割や、デコレーターを使ってきれいで読みやすいコードを書く方法を紹介します。
デコレーターとは
デコレーターは、コードの一部をほかのコードでデコレート(装飾)するように包むことで、「関数合成」や「高階関数」と表現されていました。JavaScriptで実装可能で、具体的には次のようにある関数をほかの関数で包むように呼び出します。
function doSomething(name) {
console.log('Hello, ' + name);
}
function loggingDecorator(wrapped) {
return function() {
console.log('Starting');
const result = wrapped.apply(this, arguments);
console.log('Finished');
return result;
}
}
const wrapped = loggingDecorator(doSomething);
変数wrappedで新しい関数を定義しています。この関数はdoSomething関数と同じ機能を持ち、呼び出し方も同じです。doSomething関数が実行される前後のログを残すところが異なります。
doSomething('Graham');
// Hello, Graham
wrapped('Graham');
// Starting
// Hello, Graham
// Finished
JavaScriptデコレーターの使い方
JavaScriptのデコレーターは文頭に@マークを付けて、デコレートするコードの直前に記述する特殊な書き方です。
メモ:執筆時点でデコレーターは「ステージ2 ドラフト」段階なので、完成間近ですが、変更される可能性があります。
1つのコードに付加できるデコレーターの数に制限はなく、宣言した順番に適用されます。
2つはクラス本体に、1つはクラスのプロパティに対して、このクラスには3つのデコレーターが適用されます。
- @log:クラスへのすべてのアクセスを記録する
- @immutable:クラスをイミュータブルにする。Object.freezeを新しいインスタンスに適用している
- @time:メソッドの実行時間を測定して、ユニークなタグを付けて出力する
デコレーターをサポートしているブラウザーやNodeリリースは執筆時点には存在しないため、トランスパイラーが必要です。
Babelならtransform-decorators-legacyプラグインをインストールするだけで利用できます。
メモ:プラグインの名前に「legacy」がついているのは、Babel 5のデコレーター使用方法をサポートしているためです。最終的に標準化される方法とは異なる可能性があります。
デコレーターを使う理由
関数合成は現行バージョンのJavaScriptでも実装できますが、同じ操作をクラス本体とクラスのプロパティなど、コードのほかの部分に適用するのは難しく、実装できないこともあります。
プロポーザルのデコレーターなら、クラスとプロパティデコレーターを使って問題を回避できます。将来のJavaScriptバージョンでは、ほかのパターンにおけるデコレーターサポートも追加されるでしょう。
デコレーターを使う理由は、ラッパーを簡素なコードで書けるため、コードの意図が読み取りやすくなることです。
デコレーターのタイプ
現行バージョンはクラスとクラスのメンバーのデコレーターがサポートされています。メンバーには、プロパティ、メソッド、ゲッター、セッターが含まれます。
デコレーターは、ほかの関数を返す関数なので、デコレートされた項目は適した内容で呼び出されます。デコレーター関数は読み込み時に一度だけ実行され、デコレートされたコードはその戻り値に置換されます。
クラスメンバーのデコレーター
プロパティのデコレーターはクラスのメンバーに適用します。対象はプロパティ、メソッド、ゲッター、セッターです。
デコレーター関数は3つのパラメーターを渡します。
- target:メンバーが属するクラス
- name:クラスのメンバー名
- descriptor:メンバーのデスクリプター。実際はObject.definePropertyに渡されるオブジェクト
代表的な使用例として@readonlyを示します。@readonlyの実装はシンプルです。
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
デスクリプターのプロパティ「writable」フラグにfalseを設定します。
これでクラスプロパティを次のように使用できます。
class Example {
a() {}
@readonly
b() {}
}
const e = new Example();
e.a = 1;
e.b = 2;
// TypeError: Cannot assign to read only property 'b' of object '#<Example>'
このコードはデコレートされた関数の機能を書き換えられるため、改良の余地があります。インプットとアウトプットのすべてをログに出力しましょう。
function log(target, name, descriptor) {
const original = descriptor.value;
if (typeof original === 'function') {
descriptor.value = function(...args) {
console.log(`Arguments: ${args}`);
try {
const result = original.apply(this, args);
console.log(`Result: ${result}`);
return result;
} catch (e) {
console.log(`Error: ${e}`);
throw e;
}
}
}
return descriptor;
}
インプットをログに出力し、元のメソッドを呼び出して、アウトプットをログに出力するメソッドに置き換えました。
スプレッド演算子で自動的にすべての引数を配列に変換しています。旧来のarguments変数が進化した形です。
定義したlogデコレーターは以下のように使えます。
class Example {
@log
sum(a, b) {
return a + b;
}
}
const e = new Example();
e.sum(1, 2);
// Arguments: 1,2
// Result: 3
デコレートされたメソッドの実行には、上記サンプルのように、少し変わった記述が必要です。詳しく説明するにはもう一記事必要ですが、簡潔に言うと、apply関数でthisと引数を指定して関数を呼び出します。
さらに発展させて、デコレーターに引数を渡します。以下を参考にlogデコレーターを修正してください。
function log(name) {
return function decorator(t, n, descriptor) {
const original = descriptor.value;
if (typeof original === 'function') {
descriptor.value = function(...args) {
console.log(`Arguments for ${name}: ${args}`);
try {
const result = original.apply(this, args);
console.log(`Result from ${name}: ${result}`);
return result;
} catch (e) {
console.log(`Error from ${name}: ${e}`);
throw e;
}
}
}
return descriptor;
};
}
複雑なコードになりましたが、要点は2つです。
- 引数がname1つだけのlog関数
- 関数がデコレーター関数を返す
改良前のlogデコレーターとの違いは、引数nameを外側の関数で使用している点です。
作成したsome tagのログ出力を識別できるようになりました。
log('some tag')関数は読み込んだ直後にJavaScriptランタイムで実行され、結果をデコレーターとしてsumメソッドに使います。
クラスのデコレーター
クラスデコレーターは1回でクラス全体に適用されます。デコレーター関数に、デコレートするクラスのコンストラクターを引数として渡します。
デコレートされるのはコンストラクターで、クラスのインスタンスではありません。インスタンスの修正は、デコレートされたコンストラクターを返して、追加すると更新します。
クラスデコレーターはメンバーデコレーターほど有用ではなく、関数で代用できます。クラスデコレーターを使うには、新しいコンストラクターを返して、元のコンストラクターを置き換えます。
ログ出力のサンプルに戻って、コンストラクターの引数を出力するデコレーターを作成します。
function log(Class) {
return (...args) => {
console.log(args);
return new Class(...args);
};
}
クラスを引数として受け取り、コンストラクターとして機能する新たな関数を返します。引数をログ出力して、コンストラクトしたクラスインスタンスを返すのです。
Exampleクラスをコンストラクトすると、指定した引数がログ出力され、Exampleのインスタンスが返されます。意図した通りに動きました。
クラスデコレーターに引数を持たせる方法はメンバーデコレーターと同じです。
function log(name) {
return function decorator(Class) {
return (...args) => {
console.log(`Arguments for ${name}: args`);
return new Class(...args);
};
}
}
@log('Demo')
class Example {
constructor(name, age) {}
}
const e = new Example('Graham', 34);
// Arguments for Demo: args
console.log(e);
// Example {}
実際の使用例
コアデコレーター
優れたライブラリー「Core Decorators」を紹介します。すぐに使える便利で汎用的なデコレーターが用意されています。
メソッド呼び出しのタイミング、機能廃止の警告、読み取り専用の保証など、便利で汎用的な機能を、きれいなデコレーター構文で実装できます。
React
Reactは高階コンポーネントのコンセプトをうまく取り入れたライブラリーです。関数で記述して、ほかのコンポーネントを包み込むReactコンポーネントです。
少し変更するだけでデコレーターとして使えます。Reduxライブラリーには、ReactコンポーネントをReduxストアに接続するconnect関数なら、通常は以下のように使用しますが、
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
デコレーターを使うと、次のように書き換えられます。
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
動作は完全に同じです。
MobX
MobXライブラリーはデコレーターをふんだんに使い、フィールドを簡単にObservableやComputedとマークしたり、クラスをObserverとしてマークしたりできます。
最後に
クラスメンバーデコレーターは、単独の関数に同じような方法で、コードをクラスの中に包み込めます。また多くの場面で使えるシンプルなヘルパーをきれいで読みやすいコードで記述できます。
豊かな想像力で、いろいろな場面に活用してください。
編集者メモ:読者の指摘を受けて、発表を控えたES2017にデコレーターが含まれているという記載が不正確のため、削除しました。
(原文:JavaScript Decorators: What They Are and When to Use Them)
[翻訳:内藤夏樹/編集:Livit]