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:

  1. 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.
  2. 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:

  1. We lose any help from the compiler. Nothing stops us from passing @{@"nonexistentProperty" : @1} and getting a runtime crash. As a sidenote, using NSStringFromSelector(@selector(title)) instead of a string helps, but only by a little.
  2. 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 ids, 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:

  1. 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.
  2. 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

  1. The best way to fix this issue is to extract related properties into another class, but it doesn't always make sense.