Improving Immutable Object Initialization in Objective-C
Much has been written and said about advantages of using completely immutable objects. For the past few months I’ve been making sure that as many parts as possible of systems I build are immutable. When doing that I've noticed that creation of immutable objects can become cumbersome, so I set out to improve it. You can find the outcome of my thinking in a small library called AHKBuilder. Read on to learn whys and hows behind this library.
Common patterns
Let's say we're building a simple to-do application. The application wouldn't be very useful without reminders.
So, we proceed to create a Reminder
class:
@interface Reminder : NSObject
@property (nonatomic, copy, readonly) NSString *title;
@property (nonatomic, strong, readonly) NSDate *date;
@property (nonatomic, assign, readonly) BOOL showsAlert;
@end
Since it's immutable, all its properties are readonly
, so we have to set their values without using setters, because they're unavailable.
Initializer mapping arguments to properties
The simplest way to do that, is to add an initializer:
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date showsAlert:(BOOL)showsAlert
{
self = [super init];
if (self) {
_title = title;
_date = date;
_showsAlert = showsAlert;
}
return self;
}
In most cases this kind of an initializer is all we need. It's easy to notice its drawbacks, though:
- When we add a few more properties (and it's not that hard to think of a few more for such a class) we'll end up with just too many parameters1.
- User of this initializer has to always provide all values – we can't easily enforce that by default
showsAlert
should be true; theoretically we could create another initializer:initWithTitle:date:
, but if we wanted to do that for every combination we would end up with a lot of initializers, for example for 5 properties there's 31 such combinations.
Initializer taking dictionary
Above issues can be fixed with a pattern used in Mantle. The initializer takes the following form (its implementation can be found on GitHub):
- (instancetype)initWithDictionary:(NSDictionary *)dictionary;
This way of initializating works fine in the context of Mantle, but in general has its bad points:
- We lose any help from the compiler. Nothing stops us from passing
@{@"nonexistentProperty" : @1}
and getting a runtime crash. As a sidenote, usingNSStringFromSelector(@selector(title))
instead of a string helps, but only by a little. - We have to wrap primitive types used in the dictionary.
Mutable subclass
We end up unsatisfied and continue our quest for the best way to initialize immutable objects.
Cocoa is a vast land, so we can – and should – steal some of the ideas used by Apple in its frameworks.
We can create a mutable subclass of Reminder
class which redefines all properties as readwrite
:
@interface MutableReminder : Reminder <NSCopying, NSMutableCopying>
@property (nonatomic, copy, readwrite) NSString *title;
@property (nonatomic, strong, readwrite) NSDate *date;
@property (nonatomic, assign, readwrite) BOOL showsAlert;
@end
Apple uses this approach for example in NSParagraphStyle
and NSMutableParagraphStyle
.
We move between mutable and immutable counterparts with -copy
and -mutableCopy
. The most common case matches our example: a base class is immutable and its subclass is mutable.
The main disadvantage of this way is that we end up with twice as many classes.
What's more, mutable subclasses often exist only as a way to initialize and modify their immutable versions.
Many bugs can be caused by using a mutable subclass by accident.
For example, a mental burden shows in setting up properties.
We have to always check if a mutable subclass exists, and if so use copy
modifier instead of strong
for the base class.
Builder pattern
Somewhere between initializing with dictionary and mutable subclass lies the builder pattern. First use of it that I saw in Objective-C was by Klaas Pieter:
Pizza *pizza = [Pizza pizzaWithBlock:^(PizzaBuilder *builder]) {
builder.size = 12;
builder.pepperoni = YES;
builder.mushrooms = YES;
}];
I don't see many advantages of using it in that form, but it turns out it can be vastly improved.
Improving builder pattern
First thing that we should want to get rid off is another class used just in the builder block. We can do that by introducing a protocol instead:
@protocol ReminderBuilder <NSObject>
@property (nonatomic, strong, readwrite) NSString *title;
@property (nonatomic, strong, readwrite) NSDate *date;
@property (nonatomic, assign, readwrite) BOOL showsAlert;
@end
Let's take a step back and look at the final API first:
Reminder *reminder = [[Reminder alloc] initWithBuilder_ahk:^(id<ReminderBuilder> builder) {
builder.title = @"Buy groceries";
builder.date = [NSDate dateWithTimeIntervalSinceNow:60 * 60 * 24];
}];
Instead of simply introducing a new class that conforms to this (ReminderBuilder
) protocol, we'll do something more interesting. We'll leverage Objective-C's dynamism to not create such class at all!
The initializer will be declared in a category on NSObject
, so it won't be tied to our Reminder
example:
@interface NSObject (AHKBuilder)
- (instancetype)initWithBuilder_ahk:(void (^)(id))builderBlock;
@end
Its implementation will take the following form:
- (instancetype)initWithBuilder_ahk:(void (^)(id))builderBlock
{
NSParameterAssert(builderBlock);
self = [self init];
if (self) {
AHKForwarder *forwarder = [[AHKForwarder alloc] initWithTargetObject:self];
builderBlock(forwarder);
}
return self;
}
As you can see all the magic happens in AHKForwarder
. We want AHKForwarder
to behave as if it was implementing builder protocol. As I wanted to keep the solution general I thought that I could just get the protocol name from the method signature (initWithBuilder_ahk:^(id<ReminderBuilder> builder)
). It turned out that at runtime all objects are id
s, so it's not possible.
On second thought I noticed that builder protocol declares the same properties as our immutable class, the only difference is that it uses readwrite
modifier for them. So, we don't even have to know how the builder protocol is named or what it contains! We can just assume that it declares setters for readonly
properties in the immutable class. Convention over configuration isn't that much used in Objective-C, but I think it has its place here.
Let's go step by step via AHKForwarder
source:
@interface AHKForwarder : NSObject
@property (nonatomic, strong) id targetObject;
@end
@implementation AHKForwarder
- (instancetype)initWithTargetObject:(id)object
{
NSParameterAssert(object);
self = [super init];
if (self) {
self.targetObject = object;
}
return self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
if (isSelectorASetter(sel)) {
NSString *getterName = getterNameFromSetterSelector(sel);
Method method = class_getInstanceMethod([self.targetObject class], NSSelectorFromString(getterName));
const NSInteger stringLength = 255;
char dst[stringLength];
method_getReturnType(method, dst, stringLength);
NSString *returnType = @(dst);
NSString *objCTypes = [@"v@:" stringByAppendingString:returnType];
return [NSMethodSignature signatureWithObjCTypes:[objCTypes UTF8String]];
} else {
return [self.targetObject methodSignatureForSelector:sel];
}
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
if (isSelectorASetter(invocation.selector)) {
NSString *getterName = getterNameFromSetterSelector(invocation.selector);
id argument = [invocation ahk_argumentAtIndex:2];
[self.targetObject setValue:argument forKey:getterName];
} else {
invocation.target = self.targetObject;
[invocation invoke];
}
}
@end
In methodSignatureForSelector:
we build a signature for setter using target object's (in our example, instance of Reminder
class) getter's implementation. We use mostly stuff described in Objective-C Runtime Reference, so there's no need to repeat it here.
In forwardInvocation:
we check whether a selector is a setter, and then do one of two things:
- If it is a setter, we use KVC, to set the value of a property. Reminder: KVC allows us to change values of
readonly
properties, because they're synthesized by default. - If it is not a setter, we invoke the selector on the target object. This allows getters to function properly inside the block.
And that's really all there's to it. A couple of tricks that allow us to create a simple API. We can implement copyWithBuilder:
analogously. We won't go through its source here, but you should see it on GitHub.
Summary
Finally, here's a comparison of the described builder pattern with other initialization methods:
Pros:
- allows for compile-time safe initialization of immutable objects with many properties
- it's easy to add and remove properties, change their names and types
- allows the use of default values by implementing
init
in the immutable class
Cons:
- works best with the described case: classes with
readonly
properties - doesn't support custom setter names in a builder protocol
- object passed in the block doesn't respond to
conformsToProtocol:
correctly, because we don't know the protocol's name
-
The best way to fix this issue is to extract related properties into another class, but it doesn't always make sense. ↩