Thursday, September 23, 2010

forward / method_missing with Cappuccino

Cappuccino allows to forward unhandled messages to another object, similarly to Objective-C's forward and Rails' method_missing functionalities.
However, I couldn't find any documentation on this matter, which is why I found useful to detail it a bit here. As all this information comes from message boards and mailing lists, please forgive me if some details are missing or unclear, and feel free to comment if you have deeper insight on the subject.

What is invocation forwarding?

Invocation forwarding allows an object to forward unsupported invocations to another object. In simpler terms, let say you have an object with no methods, we'll call it empty_obj, but which has forwarding set to another object, working_obj (we'll see how in a minute). When you call [empty_obj doSomething], you won't get an error like you would expect, but instead the return value of [working_obj doSomething].
This might seem useless, but can actually prove quite necessary when you use wrapper classes for instance, that should forward most methods to another object.

Let's see some code!

Let's declare the SomeWrapper class, which does nothing but include a SomeClass object.

Except for its pretty standard init: method, this class does nothing. However, all method calls will be forwarded to o, so our wrapper will look like it implements all the methods from SomeClass.
You probably guessed that it's all thanks to the implementation of methodSignatureForSelector: and forwardInvocation:, so let's see what each does and how you can use them to match your needs.

- (CPMethodSignature)methodSignatureForSelector:(SEL)selector

This method is used to determine if a selector can be sent to an object. Here, we say YES no matter what, so we delay the error to the second invocation (in forwardInvocation:).
You could also check what the selector is, for example to hide certain features of SomeClass. Doing so is as simple as comparing two strings (hint: one of them is selector), and return YES or NO to accept or revoke the call, respectively.

- (void)forwardInvocation:(CPInvocation)anInvocation

As you probably know by now, this is where the magic occurs. The anInvocation parameter is the invocation that was sent to this object. Being itself an instance of the CPInvocation class, it can me manipulated in lots of ways.
Let's have a look at its three most interesting methods: invokeWithTarget:, setReturnValue:, and setSelector:.
invokeWithTarget:, which is the method we used in our example, invokes the same selector, with the same arguments, on another target. This is the basic form of forwarding.
setSelector: is pretty self-explanatory, it will change the selector used the next time you call invoke on the object (probably at the next line). This means you can accept a selector, but forward another, which offers endless possibilities.
setReturnValue: will, well, change the return value. So now, you can have an class that returns a specific value, no matter the method invoked, simply by changing the invocation to [anInvocation setReturnValue:42] !

Of course, you can retrieve the target and selector, as well as manipulate arguments. See the Cappuccino API page for CPInvocation.

Using attributes instead of methods

As far as I know, you cannot use the forwarding mechanism with attributes. This seems to make sense, as attributes are a JavaScript feature where methods are an Objective-J enhancement.
However, what you can do is convert a method invocation to fetching an attribute. Here is how it should look like:

Instead of invoking our method on o, we retrieve the selector, use it as a parameter that o should have, and use the result as the return value.

No return!

You might find this CPInvocation handling is quite a hassle. In my setReturnValue: example, why didn't I simply use return 42 instead of calling a method on anInvocation.
Well, the answer is simple: forwardInvocation: has a return type of void, because anything you return will be lost. It is not called by Objective-J in lieu of the "real" method (the one you called but doesn't exist) the way you regularly call methods, and changing the return value won't help, either. No big deal, but pay attention to it.

However, the forward: method works in a way more similar to Objective-C, but you'll have to check the source code to see exactly how that works.

I hope you learned a powerful tool today, one that was cruelly missing from JavaScript.

No comments:

Post a Comment