— boreal-kiss.com

Higher Order Messaging (HOM)

イントロダクション

-[NSObject performSelector:withObject]に複数の引数を渡す – boreal-kiss.comで少しだけ言及していたHigher Order Messaging (HOM)の例をいくつか挙げておく。なおCocoaにおけるHOMはMetaobjectによるMPWFoundationの機能の一部として利用することができる(ようである)。

ケース1

ボタンを押す(-[AppController buttonPressed:])とアラートを表示する(-[AppController openAlert])アプリケーションを考えよう。アプリケーションのコントローラー部分は以下のように記述されているとする。

@implementation AppController
 
-(IBAction)buttonPressed:(id)sender{
	[self openAlert];
}
 
-(void)openAlert{
	NSRunAlertPanel(@"Alert", @"This is an alert", @"OK", nil, nil);
}
 
@end

1秒後にアラート表示をさせたい場合は-[NSObject performSelector:withObject:afterDelay:]を用いると以下のように書き換えることができる。

-(IBAction)buttonPressed:(id)sender{
	[self performSelector:@selector(openAlert) withObject:nil afterDelay:1.0];
}

この書き方でももちろんかまわないが、HOMを用いるとメッセージ内容がより直感的に表現される。新しくNSProxyのサブクラス(DelayedTrampoline)を導入してHOM的な書き方をした場合は以下のようになる。

#import "DelayedTrampoline.h"
 
@implementation AppController
 
//New
-(IBAction)buttonPressed:(id)sender{
	[[self afterDelay:1.0] openAlert];
}
 
-(void)openAlert{
	NSRunAlertPanel(@"Alert", @"This is an alert", @"OK", nil, nil);
}
 
//New
-(DelayedTrampoline *)afterDelay:(NSTimeInterval)delay{
	return [DelayedTrampoline delayedTrampolineWithTarget:self 
			afterDelay:delay];
}
 
@end
@interface DelayedTrampoline : NSProxy {
	id _target;
	NSTimeInterval _delay;
}
 
-(id)initWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay;
+(id)delayedTrampolineWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay;
@end
@implementation DelayedTrampoline
 
-(id)initWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay{
	_target = aTarget;
	_delay = delay;
	return self;
}
 
+(id)delayedTrampolineWithTarget:(id)aTarget afterDelay:(NSTimeInterval)delay{
	return [[[[self class] alloc] initWithTarget:aTarget 
				afterDelay:delay] autorelease];
}
 
//Override
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
	return [_target methodSignatureForSelector:aSelector];
}
 
//Override
-(void)forwardInvocation:(NSInvocation *)anInvocation{
	[anInvocation performSelector:@selector(invokeWithTarget:) 
		withObject:_target afterDelay:_delay];
}
 
-(void)dealloc{
	_target = nil;
	[super dealloc];
}
 
@end

以下に比較したい箇所だけを抜き出す。HOMの場合メッセージ(afterDelay:)の引数がメッセージ(openAlert)であるかのように書くことができることがわかる。これがHOMの”Higher Order”たる所以である。[NSObject performSelector:withObject:afterDelay:]を用いた従来の書き方よりもより直感的に理解できる表現だ。もちろん引数に書かれたメッセージは実際に引数を持つことができるため柔軟性も高い(例: -[NSObject performSelector:withObject]に複数の引数を渡す – boreal-kiss.com)。

//Normal
-(IBAction)buttonPressed:(id)sender{
	[self performSelector:@selector(openAlert) withObject:nil afterDelay:1.0];
}
 
//HOM
-(IBAction)buttonPressed:(id)sender{
	[[self afterDelay:1.0] openAlert];
}

ケース2

HOMは同じ内容を繰り返し記述しなければならない場合などにも便利だ。例えばdelegateメソッドを実行する際にターゲットであるdelegateが実際にメソッドを実装しているか確認する次のようなコードを大量に書いて(あるいはコピペして)骨が折れた経験はないだろうか。

if ([_delegate respondsToSelector:@selector(doSomething)]){
	[_delegate doSomething];
}

この場合あらかじめdelegateオブジェクトを扱うクラス(例えばAppController)にHOMを実装しておけば問題は回避可能だ。

@interface AppController : NSObject {
	id _delegate;
}
@end
#import "IfRespondsToTrampoline.h"
 
@implementation AppController
 
-(IfRespondsToTrampoline *)delegateIfRespondsTo{
	if (_delegate){
		return [IfRespondsToTrampoline 
			ifRespondsToTrampolineWithTarget:_delegate];
	}
	return nil;
}
 
@end
@interface IfRespondsToTrampoline : NSProxy {
	id _target;
}
 
-(id)initWithTarget:(id)aTarget;
+(id)ifRespondsToTrampolineWithTarget:(id)aTarget;
@end
@implementation IfRespondsToTrampoline
 
-(id)initWithTarget:(id)aTarget{
	_target = aTarget;
	return self;
}
 
+(id)ifRespondsToTrampolineWithTarget:(id)aTarget{
	return [[[[self class] alloc] initWithTarget:aTarget] autorelease];
}
 
//Override
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
	return [_target methodSignatureForSelector:aSelector];
}
 
//Override
-(void)forwardInvocation:(NSInvocation *)anInvocation{
	[anInvocation invokeWithTarget:_target];
}
 
-(void)dealloc{
	_target = nil;
	[super dealloc];
}
 
@end

以下に比較したい箇所だけ抜粋する。AppControllerに上記のようなHOMが実装されていれば、delegateオブジェクトがメソッドを実装していれば実行、なければ何もしない(実際には内部でNSProxyによるNSInvalidArgumentExceptionが挙がる)というコードが一行で書けてしまう。

//In AppController.m
 
//Normal
if ([_delegate respondsToSelector:@selector(doSomething)]){
	[_delegate doSomething];
}
 
//HOM
[[self delegateIfRespondsTo] doSomething];
1 comment
  1. […] HOMについてまとめた記事はこちら。Higher Order Messaging (HOM) – boreal-kiss.com […]

Submit comment