VirtualDelegate

NSProxyを使ったエレガントなdelegate」というのがある。

よくあるdelegateパターンだと

if([_delegate respondsToSelector: @selector(operate)])
    [_delegate operate];

という書き方をするわけだが、

    • @selector内は文字列のような扱いになるのでコンパイラチェック的にいけてない
    • メソッドチェックと実際の呼び出しで二回同じ事を書くのはいけてない

といった点が気になる。

そこで実際のターゲットオブジェクトをNSProxy派生のオブジェクトでラップし、forwardInvocation:の内部でメソッドチェックを自動化しようという考え方だ。

proxy delegateの実装(大要)

- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSel
{
    id sig = [_realObject methodSignatureForSelector: aSel];
    if(sig == nil)
    {
         // 存在しないメソッド用のダミーシグネチャ
        sig = [NSMethodSignature signatureWithObjCTypes: "@^v^c"];
    }
    return sig;
}

- (void)forwardInvocation:(NSInvocation*)invocation
{
    if([_realObject respondsToSelector: [invocation selector]])
        [invocation invokeWithTarget: _realObject];
}

これだと

[_proxyObject operate];

とするだけで実際のdelegateに当該メソッドが存在するかどうかを気にせず呼び出せるというわけである。それはいい。

しかし、delegateメソッドが何らかの値を返し、未実装時のデフォルト値を持つ場合は実際に当該メソッドが存在するかどうかが問題になる。Elegant Delegationではプロキシが – (BOOL)justRespondedというメソッドを持ち、直前の呼び出しが実際に行われたかどうかをチェックせよというのだが、どうもエレガントとは思えない。

id ret = [_proxyObject operate];
if([_proxyObject justResponded])
    // retは実際の結果
else
    // 実装はなかった。デフォルト値を使う

delegate先の有無を仮想化するというのなら、デフォルト値もproxyに持たせた方がいいんじゃないだろうか。

 // 実際のdelegateが存在すればその値、なければデフォルト値を返す
id ret = [_proxyObject operate];

要はdelegate未設定時にforwardの第二候補としてデフォルト実装を持てるようにすればいいわけだ。

実装

@interface VirtualDelegate : NSProxy
@property (readwrite,assign) id realObject;
@property (readwrite,assign) id defaultObject;
@end
@implementation VirtualDelegate
- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel
{
    id sig = [self.realObject methodSignatureForSelector: sel];
    if(sig)
        return sig;
    sig = [self.defaultObject methodSignatureForSelector: sel];
    if (sig)
        return sig;
    return [NSMethodSignature signatureWithObjCTypes: "@^v^c"];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL selector = invocation.selector;

    id realObject = self.realObject;
    if([realObject respondsToSelector: selector])
    {
        [invocation invokeWithTarget: realObject];
        return;
    }

    id defaultObject = self.defaultObject;
    if([defaultObject respondsToSelector: selector])
    {
        [invocation invokeWithTarget: defaultObject];
        return;
    }
}
@end

...
@implementation MyObject
{
    VirtualDelegate <MyProtocol> *_virtualDelegate;
}

- (id)init
{
    _virtualDelegate = (VirtualDelegate <MyProtocol>*)[VirtualDelegate alloc];
    _virtualDelegate.defaultObject = someObj; // delegateのデフォルト実装
    return self;
}

- (id)delegate
{
    return _virtualDelegate.realObject;
}

- (void)setDelegate:(id)obj
{
    _virtualDelegate.realObject = obj;
}
@end

こうすることで、delegateの存在・実装の有無に関係なく必ず妥当な値を返すVirtualDelegateが構成できる。定点通知のvoidメソッドならばdefaultObject側の実装は必要ではなく、単にinvocationが捨てられて終わりである。

ソースコード

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です