November 23, 2015

When You Are Lazy to Rewrite Your Code but Want to Have Reactiveness

Idea

Often in projects you can have a callback hell 🙂 You do. Glad if you don’t have it. And at some point of time someone comes and says: “Let’s use a new library that uses promises/futures/reactiveness, so we won’t have callback hell anymore!”

And you agree that you need to do it, but your codebase is soo large, that you cannot do migration with one change only.

[self.networkManager loginUserWithEmail:@"email" password:@"password" success:^(NSDictionary *responseDictionary) {
    [self.networkManager getUserInfoWithSuccess:^(NSDictionary *resp2) {
        if (resp2[@"invalid"]) {
            [self.networkManager logoutUserWithSuccess:^(NSDictionary *loggedOut) {
                [self handleSuccess:loggedOut];
            } failure:^(NSError *error) {
                [self handleError:error];
            }];
        } else {
            [self handleSuccess:resp2];
        }
    }  failure:^(NSError *error) {
        [self handleError:error];
    }];
} failure:^(NSError *error) {
    [self handleError:error];
}];

Here’s an implementation of the crazy idea of how you can perform such migration, so that:

  • You don’t need to rewrite everything at once;
  • You can use both variants, while you’re migrating;
  • You’ll use some magic. 🙂

Deprecate all the things

Since you’re migrating – deprecate old methods, so next time you see those methods, you will use correct implementation (probably)

- (void)getUserInfoWithSuccess:(SuccessResponse)success failure:(FailureResponse)failure __attribute__((deprecated("Use reactive implementation")));

Duplicate existing methods with promises

In case if everything is written in one style – just use rename and replace. If not – just do it manually 🙂 Here’s an example of how you can do it for current project

Find(Regexp): - \(void\)(.*?)(WithSuccess)?(Success)?(\s\w+)?:[^:]+:[^:]*;
    Replace : - (RACSignal*)$1;

Convince compiler that you know what you’re doing

We’re not going to implement all methods now, instead we’ll use forward invocation technique. Since we’re doing this, we’ll need to suppress compiler warnings

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
#pragma ide diagnostic ignored "OCUnusedMethodInspection"

@interface NetworkManager (RAC)

- (RACSignal*)loginUserWithEmail:(NSString *)email password:(NSString *)password;
...
- (RACSignal*)getUserInfo;

@end

#pragma clang diagnostic pop

Performing Runtime Magic

Since we decided not to have implementation of methods those are returning promises, in order to use Message Forwarding we would need to implement two methods:

First one is:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

Second one is:

- (void)forwardInvocation:(NSInvocation *)reactiveMethodInvocation

First one is used by runtime to understand what structure it needs to pass params to the method and return values from it. In this example we’ll have only object parameters + success and failure blocks, so it’ll be easy to construct method signature. We also will have helper method that will search for original method selector based on ‘promise’ method selector. So we’ll be able to search for

getUserInfoWithSuccess:failure:

for

getUserInfo

Once we returned valid method signature for ‘promise’ method, we’ll need to implement invocation forwarding.

Forwarding invocation

Our steps will be next:

  • Find original selector that we need to call based on ‘promise’ method
NSString *reactiveSelectorName = NSStringFromSelector([reactiveMethodInvocation selector]); 
SEL originalSelector = [self findOriginalSelectorForRACSelector:[reactiveMethodInvocation selector]];
  • Create NSInvocation with original selector
NSInvocation *originalMethodInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:originalSelector]];
originalMethodInvocation.target = self;
originalMethodInvocation.selector = originalSelector;
  • Pass all parameters to original selector invocation
// Passing all params to the original method invocation
int paramsOffset = 2; // Offset from self and _cmd params
for (int i = 0; i < paramsCount; ++i) {
    id arg;
    [reactiveMethodInvocation getArgument:&arg atIndex:i + paramsOffset];
    [originalMethodInvocation setArgument:&arg atIndex:i + paramsOffset];
}
  • Add two additional parameters (success and failure block)
  • Create a promise
  • Resolve promise with a success in success block
  • Resolve promise with an error in failure block
RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) {
    SuccessResponse successBlock = ^(id res) {
        [subscriber sendNext:res];
        [subscriber sendCompleted];
    };
    FailureResponse failureResponse = ^(NSError *error) {
        [subscriber sendError:error];
    };
    [originalMethodInvocation setArgument:&successBlock atIndex:successCallbackParamIndex];
    [originalMethodInvocation setArgument:&failureResponse atIndex:errorCallbackParamIndex];

    [originalMethodInvocation invoke];

//        id<Cancelable> cancelable;
//        [originalMethodInvocation getReturnValue:&cancelable];
//
//        return [RACDisposable disposableWithBlock:^{
//            [cancelable cancel];
//        }];
    return nil;

}] setNameWithFormat:@"RACified %@", NSStringFromSelector(originalSelector)];
  • Return promise as a value
[reactiveMethodInvocation setReturnValue:&signal];

Now you can use your manager with promises!

[[[[self.networkManager loginUserWithEmail:@"email" password:@"password"]

    // Get user info
    flattenMap:^RACStream *(id value) {
        return [self.networkManager getUserInfo];
    }]

    // Logout user if invalid or return logged in user
    flattenMap:^RACStream *(NSDictionary *resp2) {
        if (resp2[@"invalid"]) {
            return [self.networkManager logoutUser];
        } else {
            return [RACSignal return:resp2];
        }
    }]

    // Handle successful result
    subscribeNext:^(id x) {
        [self handleSuccess:x];
    } error:^(NSError *error) {
    [self handleError:error];
}];

Profit.

Notes

  •  There are a lot of ways how to do it, each one has his own pros and cons.
  • This was just an example of one possible implementation.
  • This will work only for small amount of users.
  • I would not recommend using this approach, if you aren’t covering something with tests.
  • We can easily add cancelling there.
  • This one was just a proof of concept, that this is theoretically possible to do in short period of time.
  • Make code, not war.