May 2, 2009

Dynamic Proxies

Part of our iPhone application uses proxy objects lazily load the full-fat objects behind the scenes. The initial implementation simply used an object that had the same core interface of the proxied object and used a factory to load the full-fat object in the event that a property was called on the proxy that it couldn’t service with it’s own internal state.

    @implementation MyProxyClass // : NSObject ...
    -(NSString*)doSomethingIntensive {
      if ([self fullFatObjectNotLoaded]) {
            [self loadFullFatObject];
        }
        return [fullFat doSomethingIntensive];
    }
    ...

This was all well and good - but eventually the model sitting behind the proxy grew and grew until we had a rather large hierarchy of classes that had to be managed by the proxy. To do this the proxy would have to present all of the varied properties on all of the classes in the model hierarchy. In development terms alone this wasn’t scalable - we’d keep having to add new properties to the proxy as they were added to the model; instead a shortcut around this issue had been implemented. The proxy exposed the full-fat object with it’s own getter:

    @interface MyProxyClass : NSObject ...
    -(MyClass*)getFullFat;
    ...

The problem with this is of course that now the caller to the proxy must actually know about the proxy - it can no longer just use the MyProtocol protocol (Java: ‘Interface’) and must now be aware of the type ‘MyProxyClass’. This makes the calling code tightly coupled to the lazy loading process - exactly what we don’t want.

As enterprise Java developers we see dynamic proxies intercepting calls to our classes everywhere in our EJB containers, Dependency injection frameworks (e.g: Spring, Seam), and ORM frameworks (Hibernate for example). In truth we rarely see them - perhaps in a call stack in the debugger - but we know they are there wrapping rich services around our objects without our intervention.

What I wanted was something similar for our iPhone application - some kind of Objective-C dynamic proxy that could seamlessly proxy our (perhaps overly) complex model and remain hidden from the classes who would actually be be calling the proxy.

Now writing a dynamic proxy service in Java is probably difficult. I haven’t written one but I imagine it entails reflection and byte code generation - not necessarily the stuff that the average Java developer gets stuck into. Besides - there are good libraries that do this already and do it well in the Java world. However - in the world of Objective-C it is surprisingly easy to do out of the box using a Foundation class named ‘NSProxy’.

Any selector messages (Java: ‘method calls’) invoked on the proxy invoke a default method on the proxy called ‘forwardInvocation’ including a description of the invocation (selector, arguments, etc.) By overriding the ‘forwardInvocation’ method we can route the method invocation wherever we like. In the case of our lazy loading proxy we’d like to check to see if our proxy can service the invocation (we can use 'respondsToSelector' for this) and if not - load up the object we are the proxy for and then forward the invocation to that full fat object like so:

    @implementation MyProxyClass // : NSProxy ...
  -(void)forwardInvocation:(NSInvocation*)invocation {
      SEL selector = [invocation selector];
     if ([self fullFatNotLoadedAndCanBeHandledByProxy:selector]) {
        [invocation setTarget:lowFat];
    } else if (fullFat != nil) {
       [invocation setTarget: fullFat];
    } else {
       NSAssert(FALSE, @"Hope this never happens");
     }
     [invocation invoke];
     return;
  }

Of course - it’s a bit more complicated than that. NSProxy is not an actual object as such - it’s not a subclass of NSObject. So if you send an ‘init’ invocation to a NSProxy instance it doesn’t actually call the init (Java: ‘default constructor’) on the proxy instance - it doesn’t have one. Instead this is treated like an other method and we find that we have an invocation of ‘forwardInvocation’ with selector ‘init’. And in this event what object reference should we return to the caller? It gets even more confusing when we find that calls to extremely type specific methods such as: ‘isKindOfClass’, ‘conformsToProtocol’, and ‘respondsToSelector’ also get directed straight to ‘forwardInvocation’ and that we have to handle those too! How confusing - this would never happen in Java!

So this is the way that I chose to implement the lazy loading proxy:
  • The proxy would have an internal ‘low-fat’ skeleton object that acts as a placeholder for the proxied object - in my case it had a member that held the full fat objects database ID. The low fat object is alloced when the proxy gets a request for the NSMethodSignature for the initWithId method - the proxy passes a reference to itself - not the databaseId - we have to wait for the 'forwardInvocation' call to get at the method call argument.
    @implementation MyProxyClass // : NSProxy ...
    ...
    -(NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
        if (selector == @selector(initWithId:)) {
            if (lowFat == nil) {
                lowFat = [[LowFatObject alloc] initWithProxy:self];
            } else {
                NSAssert(FALSE, @"Hope this never happens");
            }
        }
        ...
    }
    ...
    @end

    @implementation LowFatObject
    @synthesize databaseId;
    -(id)initWithProxy:(MyProxyClass*)aProxy {
        if (self = [super init]) {
            proxy = aProxy;
            // NOTE: 'proxy' member of type MyProxyClass!
        }
        return self;
    }
  • The ‘initWithId’ call to the proxy would actually be routed to the low-fat object to but note that the low fat object has already been alloced and init'ed by the proxy. So instead it adopts the databaseId value and (importantly) returns a reference to the proxy - not itself.
    @implementation LowFatObject
    -(id)initWithProxy:(MyProxyClass*)aProxy...
    ...
    -(id)initWithId:(int)aDatabaseId {
        databaseId = aDatabaseId;
        return proxy;
        // We want the caller to have a ref to the proxy!
    }
  • We're still left with the issue of deciding when to load the full-fat item. To do this we need to check if the low-fat skeleton object is able to service the invocation and if not we need to load the full-fat object and set that as the invocation target. We do this when we're asked to supply the 'NSMethodSignature' for the pending invocation. Before we dive into this recall that the invocation could be for something where the return type is extremely type specific - such as 'respondsToSelector' - in those eventualities we MUST also load the full-fat object because only the can we really know what the object we are a proxy for is actually capable of.
    @implementation MyProxyClass // : NSProxy ...
    ...
    -(NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
        ... init method handler ...
        NSObject* target;
        // Is the selector extremely type specific?
        if (selector == @selector(isKindOfClass:)
            || selector == @selector(conformsToProtocol:)
            || selector == @selector(respondsToSelector:))
        {
            [self loadFullFatTarget];
            target = fullFat;
        } else if ([self fullFatNotLoadedAndCanBeHandledByProxy:selector]) {
            // The low-fat item responds to the selector
            // and the full-fat hasn't been loaded
            target = lowFat;
        } else {
            // We need to load the full fat because the low-fat
            // skeleton can't handle it
            [self loadFullFatTarget];
            NSAssert1(
                [fullFat respondsToSelector:selector],
                @"Neither full/low fat object can handle selector %@",
                selector
            );
            target = fullFat;
        }
        return [target methodSignatureForSelector:selector];
    }

Despite being a little fiddly this approach has worked quite well in practice. As a Java developer it's quite refreshing to be able to construct something as powerful as a Dynamic Proxy using a core class supplied in the SDK. Although the model we are proxying probably has much room for refactoring we can now at least add and remove properties from the model classes and have them implicitly supported by the proxy without any further modification to the proxy itself.

No comments:

Post a Comment