Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AOP in Objective-C: Inject context-aware code into each method while maintaining DRY

UPDATE:

With some key suggestions and back and forth with George, I've come up with two different ways to achieve exactly what I want in CodeRunner and posted it on Github's gist site: Objective-C AOP gist

The code is rough because it's a new concept and I just finished at 1:30am. It definitely works though and has some niceties like auto-adding all methods that aren't initializers, getters or setters. [END UPDATE]

Several times (but certainly not very often) I've come across a situation where my code would be a bit DRYer if I could call a context-sensitive piece of code for each method in a class. Use of the Objective-C runtime is totally fine, I'd accept C or C++ solutions as well.

Instead of:

- (void)methodName1
{
   self->selector = _cmd;
   NSLog(@"This method is named: %@",_cmd);
   //more code
}

- (void)methodName2
{
   self->selector = _cmd;
   NSLog(@"This method is named: %@",_cmd);
   //more code
}

Have something like this, with the result being the same:

+ (void)AOPMethod
{
   self->selector = _cmd;
   NSLog(@"This method is named: %@",_cmd);
}

- (void)methodName1
{
   //more code
}

- (void)methodName2
{
   //more code
}

In a real-world application, AOPMethod would contain more code and there'd be more methods in the class.

P.S., I'm fairly obsessed with DRY. Along with clarity of prose and performance it's a key component of how I assess my code's quality over the long term. For each new way I can avoid repeating myself, the benefit is exponential because I break off as much code as possible in reusable classes that are shared across many projects.

like image 342
james_womack Avatar asked Feb 13 '12 20:02

james_womack


1 Answers

For the specific use-case in the question, one could provide a handler that replaces the original implementation functions and calls before/after handlers as well as the original functions using something like this approach. In general however method implementation patching won't work as one would have to provide a handler/interception method for every intercepted method signature.

What would work more general (i.e. for everything except variable argument functions) would be handling -forwardInvocation:. The problem here though is that we would have to get that method invoked in the first place. As we can't remove methods in ObjC2, that can't be done in place.

What can be done however is using proxies that implement forwardInvocation: and call our before/after handlers.

@interface AspectProxy : NSProxy {
    id target_;
}
- (id)initWithTarget:(id)target;
@end

@implementation AspectProxy
- (id)initWithTarget:(id)target {
    target_ = [target retain];
    return self;
}
- (void)dealloc {
    [target_ release];
    [super dealloc];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [target_ methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)inv {
    SEL sel = [inv selector];
    NSLog(@"forwardInvocation for: %@", NSStringFromSelector(sel));
    if (sel == @selector(aspectBefore:) || sel == @selector(aspectAfter:)) {
        return;
    }
    if ([target_ respondsToSelector:@selector(aspectBefore:)]) {
        [target_ performSelector:@selector(aspectBefore:) withObject:inv];
    }
    [inv invokeWithTarget:target_];
    if ([target_ respondsToSelector:@selector(aspectAfter:)]) {
        [target_ performSelector:@selector(aspectAfter:) withObject:inv];
    }
}
@end

As we don't need to return the actual instance from an init method, this could even be done transparently:

@interface Test : NSObject
- (void)someFunction;
@end

@implementation Test
- (id)init {
    if (self = [super init]) {
        return [[AspectProxy alloc] initWithTarget:[self autorelease]];
    }
    return self;
}
- (void)aspectBefore:(NSInvocation *)inv {
    NSLog(@"before %@", NSStringFromSelector([inv selector]));
}
- (void)aspectAfter:(NSInvocation *)inv {
    NSLog(@"after %@", NSStringFromSelector([inv selector]));
}
- (void)someFunction {
    NSLog(@"some function called");
}
@end

Now the following code:

Test *x = [[[Test alloc] init] autorelease];
[x someFunction];

... will print:

forwardInvocation for: someFunction
before someFunction
some function called
after someFunction

A runnable sample can be found in this gist.

like image 59
Georg Fritzsche Avatar answered Nov 09 '22 16:11

Georg Fritzsche