Elegant Delegation


Special Guest Rant by AgentM, on March 11, 2005

If you have ever been faced with the design of a complicated object, you know how helpful delegation can be. Most of the Apple-provided user interface elements use or require delegate handlers. Using delegation instead of subclassing allows for a single central controller object. For example, in an NSDocument application, there is usually a document-controller. Using delegation and data sources for the UI elements, it is easy to use the one central controller to handle the UI elements. If such features required subclasses, there would be many decoupled objects for every UI element that exists.

Using delegation may look easy, but when implementing delegate management, it is easy to overlook many things. This article presents some tips on getting it to work properly. As a bonus, it includes an NSProxy subclass which forwards messages to an object if-and-only-if the proxied class responds to the selector.

The Problem

Developing a delegate interface is the most difficult part of the delegation implementation. The third-party programmer relies on the ability to catch the most common events occuring to the object supporting the delegation. If you are not careful here, the delegate won't be useful enough and subclassing will be necessary every time, so cover all the bases. Some tips:

Delegate handling is often implemented like this:

...
if([_delegate respondsToSelector(gonk:somethingHappened:)])
    [_delegate gonk:self somethingHappened:YES];
...

Clearly you can see why this is error-prone. It is too easy to have a typo in the selector name and the selector has to be typed twice. Also, if _delegate is nil, the message is silently ignored.

Well, that is a lot to keep track of. Wouldn't it be handy if we could handle all of this using a simple object which the delegation object interacts with?

MDelegateManager.h

MDelegateManager is a simple NSProxy subclass which accepts any message passed to it. If the proxied object it represents responds to the passed message, it is forwarded to that object. Otherwise, it does nothing. Remember that nil accepts any message and does nothing. This is very similar except that MDelegateManager represents another object, which is what is meant by proxy.

Following is the interface for the class:

@interface MDelegateManager : NSProxy {
	id _proxiedObject;
	BOOL _justResponded,_logOnNoResponse;
}

-(void)forwardInvocation:(NSInvocation*)invocation;
-(id)proxiedObject;
-(void)setProxiedObject:(id)proxied;
-(BOOL)justResponded;
-(void)setLogOnNoResponse:(BOOL)log;
-(BOOL)logOnNoResponse;
@end

Note that this class inherits from NSProxy which is not a subclass of NSObject. This means that NSProxy is a separate root class of Objective-C. NSProxy is designed for situations where an object is needed to represent a remote object (such as with Distributed Objects) or an expensive object.

Our represented object is neither remote nor expensive, so it is used here because it implements the minimum number of methods to be a root class, reducing method conflicts between the represented object and the NSProxy subclass. For example, NSProxy does not implement -description, so there is no ambiguity- -description will be sent to the represented object.

In any case, we have methods to set and return the proxied object (the actual delegate object) and some more methods which will be explained later.

You may have heard of message forwarding for Objective-C. It works, but because messages are not first-class objects in Objective-C, it is not easy to grasp. An actual message call to a specific object is represented by an NSInvocation object and a message is represented by an NSMethodSignature object which describes a message's name and arguments to the runtime.

When an object is sent a message which it does not implement, it is given a chance to respond through the -methodSignatureForSelector: and -forwardInvocation: methods. -methodSignatureForSelector: must return the NSMethodSignature associated with the selector. In the delegation case, the signature is retrieved from the delegate. -forwardInvocation: is then given the opportunity to send the message to the correct object.

Let's take a look at how they are used:

@interface NSMethodSignature (objctypes)
+(NSMethodSignature*)signatureWithObjCTypes:(const char*)types;
@end

@implementation MDelegateManager
-(id)init
{
	_proxiedObject=nil;
	_justResponded=NO;
	_logOnNoResponse=NO;
	return self;
}

Unforunately, in order to get this object to work properly, the use of an undocumented method which creates an NSMethodSignature from a C string is required. You will see shortly why this is necessary. The category on NSMethodSignature is necessary to eliminate the unknown message compiler warning. The -init method above effectively does nothing. Note that NSProxy does not implement -init, so we do not call [super init].

-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
	NSMethodSignature *sig;
	sig=[[_proxiedObject class] instanceMethodSignatureForSelector:selector];
	if(sig==nil)
	{
		sig=[NSMethodSignature signatureWithObjCTypes:"@^v^c"];		
	}
	_justResponded=NO;
	return sig;
}

-methodSignatureForSelector is called when a compiled definition for the selector cannot be found. This is where we use the secret, undocumented method to create our own dud NSMethodSignature. This is necessary because if this method returns nil, a selector not found exception is raised. The string argument to -signatureWithObjCTypes: outlines the return type and arguments to the message. To return a dud NSMethodSignature, pretty much any signature will suffice. Since the -forwardInvocation call will do nothing if the delegate does not respond to the selector, the dud NSMethodSignature simply gets us around the exception.

-(void)forwardInvocation:(NSInvocation*)invocation
{
	if(_proxiedObject==nil)
	{
		if(_logOnNoResponse)
			NSLog(@"Warning: proxiedObject is nil! This is a debugging message!");
		return;
	}
	if([_proxiedObject respondsToSelector:[invocation selector]])
	{
		[invocation invokeWithTarget:_proxiedObject];
		_justResponded=YES;
	}
	else if(_logOnNoResponse)
	{
		NSLog(@"Object \"%@\" failed to respond to delegate message \"%@\"! This is a debugging message.",[[self proxiedObject] class],NSStringFromSelector([invocation selector]));
	}
	return;
}

Following the code, we have a special case for _proxiedObject being nil. Specifically, if our debug variable _logOnNoResponse is on, we print some potentially helpful information about the condition. This relieves the "why-isn't-the-method-getting-called" syndrome when the programmer (for example, AgentM) forgets to set the delegate. However, it is not invalid to have a nil delegate, so we just return.

Next, if the _proxiedObject does implement the requested selector, we invoke the invocation and set the special variable _justResponded. This variable can be accessed by the object using this class to check if the delegate actually responded and behave accordingly- this is useful in some limited circumstances. Continuing on, if the proxied object does not respond to the selector and the logging variable is true, then we print an informative debugging message.

The rest of the code is standard boiler-plate accessors:

-(id)proxiedObject
{
	return _proxiedObject;
}

-(void)setProxiedObject:(id)proxied
{
	_proxiedObject=proxied; //do not retain- could create circular references
}

-(BOOL)justResponded
{
	return _justResponded;
}

-(void)setLogOnNoResponse:(BOOL)log
{
	_logOnNoResponse=log;
}

-(BOOL)logOnNoResponse
{
	return _logOnNoResponse;
}
@end

One thing to note here is that the delegate is not -retained. This is because it could cause circular cases of retention.

So let us use the new class which makes delegation a breeze! The following code demonstrates how an object might use MDelegateManager.

//****Gonk.h

@interface Gonk : NSObject
{
    MDelegateManager *_delegate;
}
...
@end

//****Gonk.m

@interface MDelegateManager (GonkDelegate)
-(void)gonk:(Gonk*)gonk somethingHappened:(BOOL)continue
@end

@implementation Gonk
-(id)init
{
    ...
    _delegate=[[MDelegateManager alloc] init];
    [_delegate setLogOnNoResponse:YES];
    ...
}

-(void)setDelegate:(id)delegate
{
    [_delegate setProxiedObject:delegate];
}

-(id)delegate
{
    return [_delegate proxiedObject];
}

-(void)somethingHappened
{
    ...
    [_delegate gonk:self somethingHappened:YES];
    //Wow! That's it!
    ...
}
@end

From the code, you can plainly see that calling a delegate is now as simple as sending any other message. Take a look at the GonkDelegate category on MDelegateManager. This squelches compiler warnings concerning undeclared messages being sent later in the code.

Nota Bene

There are still unanswered questions here. How is deallocation handled? NSProxy does not respond to -retain or -release, but it doesn't matter- these messages are forwarded through to the delegate (probably not very useful). However, because MDelegateManager does not use the retain count method, it must be explicitly -deallocated. So the following method should be added to class Gonk.

-(void)dealloc
{
    [_delegate dealloc]; //special case!
    [super dealloc];
}

Note that we must call -dealloc here because NSProxy does respond to it and the message will not be forwarded. Alternatively, we could implement retention count on the subclass, but that would be more work for not much benefit.

The second question is: what if I want the delegate message to return a value? That is what the _justResponded variable is for; by checking if the delegate actually responded, then the return value is valid. Some demonstration code:

//delegate implements -(BOOL)gonk:(Gonk*)gonk somethingHappened:(BOOL)itDid;
//Gonk.m
-(BOOL)somethingHappened
{
    BOOL ret;
    ret=[_delegate gonk:self somethingHappened:YES];
    if([_delegate justResponded])
        return ret;
    return NO;
}

If the delegate did respond, then it is safe to use what it returned, if not, don't use it. The Objective-C genius will note that, for a BOOL, using the value from a failed -forwardInvocation: is safe, however, the method above extends to other types such as structures.

Conclusion

Using some NSProxy magic, it's not hard to make delegation really elegant and very simple. It would be very simple to add extensions to this class. One useful extension could be posting a notification based on the forwarded selector so that a simple [_delegate gonk:self message:YES] messages the delegate and constructs and posts a notification.

Stay tuned to this website for more useful NSProxy subclasses!

This article is in the public domain.



borkware home | products | miniblog | rants | quickies | cocoaheads
Advanced Mac OS X Programming book

webmonster@borkware.com