Objective-CのMessage Forwarding

Objective-Cではオブジェクトに存在しないメッセージを送ると例外が発生します。しかし、Message Forwardingと呼ばれる機能を用いるとそのような場合に例外を出すことなくメッセージを処理することが出来るようになります。これは、PerlのAUTOLOADと同じと考えて良いようです。

で、このMessage Forwardingなんですが、恐らくフレームワークやライブラリなどを設計する場合には強力な助けとなる機能のようですが、アプリケーションを作る場合にはあまり使う場面は少なそうです。Appleドキュメントでは継承の代わりに使う例が紹介されてますが、「Modern Perl」のAUTOLOADのサンプルで出ていた「メソッド呼び出しを記録するProxyクラス」という例も面白いものなので、これをObjective-Cで書き直したものとして紹介したいと思います。

このProxyクラスの名称はLogProxyとします。LogProxyは以下のように使うことを想定してます。

// Testクラスのメソッド呼び出しを記録する
id test = [[LogProxy alloc] initWithTarget:[[Test alloc] init]];
[test someMethod];   // ログに記録される

ではLogProxyの実装を見てみましょう。まずインターフェース部分。

@interface LogProxy : NSObject {
    id proxied;
}
- (id)initWithTarget:(id)target;
- (void)dealloc;

@end

Message Forwardingの転送先としてproxiedという変数を保持するようにします。また、proxiedはコンストラクタで与えるようにしてます。

次に実装部分です。Message Forwardingを行うためには、forwardInvocation:とmethodSignatureForSelector:という2つのメソッドを実装する必要があります。forwardInvocation:は存在しないメッセージ呼び出しがあったときに代わりに呼ばれるメソッドで、引数としてNSInvocationのオブジェクトを取り、そこから呼び出されたときのセレクタの情報などを取得することができます。また、methodSignatureForSelector:はメソッドシグネチャを返すもののようです。あまりこちらは理解できてないのですが、親クラスのものか転送先オブジェクトのそれを返すことでよいようです。実装のコードは次のようになります。

@implementation LogProxy

- (id)initWithTarget:(id)target {
    if ((self = [super init]) != nil) {
        proxied = target;
    }
    return self;
}

- (void)dealloc {
    [proxied release];
    [super dealloc];
}

// 存在しないメッセージが送られたときに呼ばれる
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 関数呼び出しのログを記録する
    NSLog(@"Called [%@]", NSStringFromSelector([anInvocation selector]));
    // メッセージを転送
    [anInvocation invokeWithTarget:proxied];
}

// こちらは決まりきったコード(?)
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
    if ([super respondsToSelector:aSelector])
        return [super methodSignatureForSelector:aSelector];
    return [proxied methodSignatureForSelector:aSelector];
}

@end

以上。基本的には「Modern Perl」に出ていたもののマネですが、転送をプロクシ的に使うというのは何か別の応用も考えられるかもしれません。