I’m slowly starting to appreciate the role of metaprogramming in Objective-C and how to do it in a compiler-safe manner. With dynamic methods, there are are two cases: you can either send a message that the receiver doesn’t necessarily know about at compile-time, or you can receive a message that the sender might know about at compile time.
Dynamic Method Resolution
Receiving a message that you didn’t publicize at compile-time is called dynamic method resolution. Rails uses this to great effect with dynamic finders. They let you call User.find_by_email('soroush@khanlou.com')
. That message will be caught at run-time in the User
class and dynamically return the result of User.find_by('email', 'soroush@khanlou.com')
. This would automatically work for every property you have on your model object. Strictly speaking, this is possible in Objective-C (I show how exactly in an article about dynamic Objective-C on the Rap Genius tech blog). However, there are a number of things in Objective-C that prevent us from writing code like this, including
- ARC, which requires the method signature of every method to be known at compile-time, so that clang can insert retains and releases at the right time
- compiler warnings, which assure that the methods we are calling are actually publicized (in a header) and that the methods that we publicize actually exist
Dynamic Message Construction
On the other hand, you can have a method that the sender builds at run-time but the receiver has already defined at compile-time. This is called dynamic message construction, and, crucially, the compiler won’t stand in our way for this technique.
You can see dynamic message construction at play in Core Data’s validators. Core Data will dynamically check for and call methods of the form validate:error:
. For example, if you had a “name” property, Core Data would dynamically call -(BOOL)validateName:(id *)ioValue error:(NSError **)outError
, giving you the power to validate it, return an error describing why it’s not valid, or even change the pointer to the object its asking you to validate. This could all be rolled up into one big method called -validateValue:forKey:withError:
, but that could grow to be a pretty big method! You’d probably want to separate each of the validations like so:
- (void)validateValue:(id *)value forKey:(NSString *)key withError:(NSError *)error {
if ([key isEqualToString:@"name"]) {
[self validateName:value error:outError]
}
if ([key isEqualToString:@"email"]) {
[self validateEmail:value error:outError]
}
}
A new refactoring
To that end, I present a new refactoring strategy: Replace Conditional With Message Construction.
Martin Fowler’s book, Refactoring, in addition to outlining the intention behind refactoring and the methodology for doing it safely, also includes a catalog of refactoring strategies (including Replace Conditional With Polymorphism, which is what inspired this blog post).
Core Data shows us how this is used in the model layer, but let’s look at an example in the view layer as well.
In the Rap Genius app, we use -configureCell:atIndexPath:
when setting up cells to separate the creation of the cell from its configuration. If a table view has many types of objects represented in its rows, we want to break down our configuration method even further:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
id object = [dataSource objectAtIndexPath:indexPath];
if ([object isKindOfClass:[RGArtist class]]) {
[self configureCell:cell withArtist:object];
} else if ([object isKindOfClass:[RGSong class]]) {
[self configureCell:cell withSong:object];
}
}
As this gets more and more complex, it is a ripe place to use this refactoring.
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
id object = [dataSource objectAtIndexPath:indexPath];
SEL configurationSelector = [inflector selectorWithPrefix:@"configureCell:with" propertyName:[[object class] modelName] suffix:@":"];
if ([self respondsToSelector:configurationSelector]) {
[self performSelector:configurationSelector withObject:cell withObject:object];
} else {
[self configureCell:cell withObject:object];
}
}
This code will now dynamically get the modelName
of your model (such as “Song”), and call configureCell:withSong:
or configureCell:withArtist:
as appropriate.
The great thing about this pattern is that it helps separate your code into very logical units without having to include a giant conditional that is brittle and prone to change. It truly shines in a framework, where you may not know what objects are going to be represented in your table view, and it becomes crucial to call their configuration methods dynamically.