3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Decoratorで非同期コールバックをAOPするよ

Last updated at Posted at 2015-11-22

唐突ですがTypeScriptいいですね。最近はクライアントもサーバもほぼすべてTypeScriptになってきました。
もともとは静的型付けでバグを減らそう的なニュアンスでしか使っていなかったんですが、使い方次第でどんどん快適になっていきますね。

ということで、今回はJavaScriptにおける非同期処理の問題を1.5系から利用可能になったDecoratorを用いて解決していくぞ!と鼻息荒くお届けいたします (・∀・)
※ 間違いや、もっとこうしたら :thumbsup: 的なことあれば是非ご意見頂ければ嬉しいです!
※ ここではDecoratorの詳細には触れませんのであしからず。

非同期処理の問題点

一般的にTypeScriptを使った開発では言語仕様の恩恵を受けオブジェクト化されたとても見やすいコードになると思いますが、非同期処理が入ってきた途端、コードが汚くなっていくように思えます。汚くなっていく原因は下記の要素をTypeScriptが解決しきれていないからなのではと(TypeScriptの問題と言ってしまうのは大げさ過ぎるとかもですが)。
また、GeneratorやPromiseを使えば解決(もしくは軽減)するという意見もあるかもですが、それらを用いても余計なコードが実装に入り混んでしまうのでは無いでしょうか。

コールバック地獄

非同期処理によってthisスコープが変わることはJavaScriptからお馴染みですが、これまでのJavaScriptでの解決策をそのままTypeScriptのクラス構造に当てはめて実装してしまうと、機能をメソッド化して切り出している所をわざわざコールバック関数だけはメソッド内にクロージャ化して書いたり(再利用できない)、コールバック関数に渡したい変数がある場合は一度引数を含めてクロージャ化して(bindを使って予め引数を渡して束縛した状態でコールバック関数を作ったり)非同期処理元に渡したりと努力が必要ですね。
TypeScriptはアロー関数式を用いることである程度これら問題を解決できますが、アロー関数式で定義されたメソッドは今現在MethodDecoratorが適用できないようですし、ビルド後のJavaScriptコードではアロー関数式で定義したメソッドは他のメソッドと異なりprototypeメンバにはならずコンストラクタ内でクロージャ化されてしまったり(沢山インスタンス化されるようなオブジェクトだとメモリを浪費する原因に)、見た目が複雑になり直感的ではない(あくまで個人的な見解)という幾つかの問題もありますので、可能な限りアロー関数式は使いたくないものですね。

地獄例 (1)

非同期処理のネストで複雑化しテストもやりにくい


class Hoge {
	constructor(private helloGreeting : string, private hiGreeting : string){
	}
	say(name : string){
		var _this = this;
		setTimeout(function(){
			alert(`${_this.helloGreeting} ${name}`);
			setTimeout(function(){
				alert(`${_this.hiGreeting} ${name}`);
			},0);
		},0);
	}
}

new Hoge('Hello','hi').say('hoge');

このテストは書きたくない(´・ω・`)

地獄例 (2)

アロー関数式を使ってすっきりしたが、ビルドの結果メソッドがコンストラクタでクロージャ化されてしまった。

class Hoge {
	private name : string;
	
	constructor(private helloGreeting : string, private hiGreeting : string){
	}
	say(name : string){
		this.name = name;
		setTimeout(this.hello,0);
	}
	
	hello = () => {
		alert(`${this.helloGreeting} ${this.name}`);
		setTimeout(this.hi,0);
	}
	
	hi = () => {
		alert(`${this.hiGreeting} ${this.name}`);
	}
}

new Hoge('Hello','hi!').say('hoge');

ビルド後 (JavaScriptコード)

var Hoge = (function () {
    function Hoge(helloGreeting, hiGreeting) {
        var _this = this;
        this.helloGreeting = helloGreeting;
        this.hiGreeting = hiGreeting;
        this.hello = function () {
            alert(_this.helloGreeting + " " + _this.name);
            setTimeout(_this.hi, 0);
        };
        this.hi = function () {
            alert(_this.hiGreeting + " " + _this.name);
        };
    }
    Hoge.prototype.say = function (name) {
        this.name = name;
        setTimeout(this.hello, 0);
    };
    return Hoge;
})();
new Hoge('Hello', 'hi!').say('hoge');

prototypeメンバになっているのはsayだけだ(´・ω・`)

地獄例 (3)

引数の丸め込み(下記の例であればbindの利用でもOKか)

class Hoge {
	private name : string;
	
	constructor(private helloGreeting : string, private hiGreeting : string){
	}
	say(name : string){
		setTimeout(this.hello(name),0);
	}
	
	hello = (name : string) => {
		var f = () =>{
			alert(`${this.helloGreeting} ${name}`);
			setTimeout(this.hi(name),0);
		}
		return f;
	}
	
	hi = (name : string) => {
		var f = () => {
			alert(`${this.hiGreeting} ${name}`);
		}
		return f;
	}
}
new Hoge('Hello', 'hi!').say('hoge');

hellohiもCallback関数の生成機能になって(名が体をなしていないし)再利用しにくい(´・ω・`)

非同期処理時のエラー処理(例外)問題

JavaScriptの非同期処理における例外の扱いについてはここでは詳しく述べませんが、同期的に行われている処理と同じように記述することができず、コードが煩雑になりやすいですよね。

例外が思うように補足できない

class Hoge {
	private name : string;
	
	constructor(private helloGreeting : string, private hiGreeting : string){
	}
	say(name : string){
		try{
			setTimeout(this.hello(name),0);
		}catch(e){
			//ここではhiで発生した例外は捕捉できない
			console.log(e.message);
		}
	}
	
	hello = (name : string) => {
		var f = () =>{
			alert(`${this.helloGreeting} ${name}`);
			try{
				setTimeout(this.hi(name),0);
			}catch(e){
				//もちろんここでもhiで発生した例外は捕捉できない
				console.log(e.message);
			}
		}
		return f;
	}
	
	hi = (name : string) => {
		var f = () => {
			alert(`${this.hiGreeting} ${name}`);
			throw new Error('error has occurred.');
		}
		return f;
	}
}

try{
	new Hoge('Hello', 'hi!').say('hoge');
}catch(e){
	// なんだったらここでもhiの例外は捕捉できない
	console.log(e.message);
}

これらコールバック関数内での例外もcatchするのであれば、それぞれのコールバック関数内でそれぞれ良しなに処理する必要があると思われます。
良しなに書いてみたのが下記です(非同期で呼ばれた関数内で起きた例外は統一的に特定のメソッド流したいという仮定の元)。

class Hoge {
	private name : string;
	
	constructor(private helloGreeting : string, private hiGreeting : string){
	}
	say(name : string){
		setTimeout(this.hello(name),0);
	}
	
	hello = (name : string) => {
		var f = () =>{
			try{
				alert(`${this.helloGreeting} ${name}`);
				setTimeout(this.hi(name),0);
			}catch(e){
				this.errorCallback(e);
			}
		}
		return f;
	}
	
	hi = (name : string) => {
		var f = () => {
			try{
				alert(`${this.hiGreeting} ${name}`);
				throw new Error('error has occurred.');
			}catch(e){
				this.errorCallback(e);
			}
		}
		return f;
	}
	
	errorCallback(e : Error){
		console.log(e.message);
	}
}
new Hoge('Hello', 'hi!').say('hoge');

これだとコールバック関数それぞれにtry...catchを記述していく必要があって面倒(´・ω・`)

Decoratorを用いてAOPする

ということで、下の結論に持っていくために若干事を大げさに荒立ててまいりましたが、Decoratorを使って上記問題を解決してみたいと思います。(・∀・)
と言っても特定の条件の元でしか考えていないので、使えないシチュエーションもあるかもです...。その場合は一応教えて下さいませ。

サンプル

Decorator実装

/**
* Async callback decorator
**/
let callback = (error? : string) => {
	return (target : any ,name : string ,descriptor : PropertyDescriptor) => {
		let delegate = descriptor.value;

		// AOPする	
		descriptor.value = function(){
			var _this = this;
			var args = arguments;
			var f = function(){
				try{
					// 呼び元と同じthisスコープで呼び出す
					let result = delegate.apply(_this, args);
					return result;
				}catch(e){
					if(error && target[error]){
						// 指定されたエラーコールバック先を呼び出す
						target[error].call(_this,e);
					}else{
						throw e;
					}
				}
			} 
			return f;
		};
		
		//他のDecoratorで何かされている可能性もあるので
		for(let key in delegate){
			descriptor.value[key] = delegate[key];
		}
		
		return descriptor;
	} 
}

errorコールバック先の受け方(メソッド名を受け取る)がダサいです(´・ω・`)
もっと良いやり方あったら教えて下さいませ。

Decorator 利用側サンプル

/**
* Sample Calss
**/
class Hoge {
	constructor(private helloGreeting, private hiGreeting){
	}
	
	public say(name){
		setTimeout(this.hello(name),0);
	}
		
	@callback('errorCallback')
	private hello(name : string){
		console.log(`${this.helloGreeting} ${name}`) ;
		setTimeout(this.hi(name),0);
	}

	@callback('errorCallback')
	private hi(name : string){
		console.log(`${this.hiGreeting} ${name}`) ;
		throw new Error('error has occurred.');
	}
	
	errorCallback(e){
		console.log(`${e.message} [ Greeting Message : ${this.greeting}]`);
	}
}

すべて含めたサンプル

複数のDecoratorと強調して動作しているかを試すために目的と少し逸脱した処理も入れてみました

let callable = (target : any ,name : string ,descriptor : PropertyDescriptor) => {
	let val = descriptor.value;
	val.__callable = true;
}

let debug = (target : any ,name : string ,descriptor : PropertyDescriptor) => {
	let delegate = descriptor.value;

	descriptor.value = function(){
		let args : string[] = [];
		for(let i in arguments){
			args.push(arguments[i]);
		}
		console.log(`[debug] ${name} in: ${args.join()}`);
		let result = delegate.apply(this, arguments);
		console.log(`[debug] ${name} out: ${result}`);
		return result;
	};
	
	for(let key in delegate){
		descriptor.value[key] = delegate[key];
	}
	
	return descriptor;
}

let callback = (error? : string) => {
	return (target : any ,name : string ,descriptor : PropertyDescriptor) => {
		let delegate = descriptor.value;
	
		descriptor.value = function(){
			var _this = this;
			var args = arguments;
			var f = function(){
				try{
					let result = delegate.apply(_this, args);
					return result;
				}catch(e){
					if(error && target[error]){
						target[error].call(_this,e);
					}else{
						throw e;
					}
				}
			} 
			return f;
		};
		
		for(let key in delegate){
			descriptor.value[key] = delegate[key];
		}
		
		return descriptor;
	} 
}


/**
* Sample Class
**/

class Hoge {
	constructor(private helloGreeting, private hiGreeting){
	}
	
	public say(name){
		setTimeout(this.hello(name),0);
	}
	
	@debug
	@callable
	@callback('errorCallback')
	private hello(name : string){
		alert(`${this.helloGreeting} ${name}`) ;
		setTimeout(this.hi(name),0);
	}

	@debug	
	@callable
	@callback('errorCallback')
	private hi(name : string){
		alert(`${this.hiGreeting} ${name}`) ;
		throw new Error('error has occurred.');
	}
	
	errorCallback(e){
		console.log(`${e.message} [ Greeting Message : ${this.helloGreeting} & ${this.hiGreeting}]`);
	}
}

new Hoge('こんにちは','hi!!').say('ふが')

長くなったけど、これでどうも目的のことはできそうだ(・∀・)
これでもまだまだ解決できていない問題はあるかと思いますが、それはまた後日。

3
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?