We are all aware of the advantages of immutable objects. These is often a desire to make all or most model objects immutable, but they tend to have a lot of properties. How do you initialize such objects without creating a telescoping initializer?
Immutable Objects in Cocoa #
The first idea that comes to mind is to follow the steps of the platform. Cocoa
objects often have an immutable and mutable counterpart. A good example of an immutable object is a NSURLRequest
which has lots of properties. To construct it, you use its mutable counterpart NSMutableURLRequest
. NSMutableURLRequest
is a subclass of NSURLRequest
which in turn implements NSCopying
and NSMutableCopying
protocols. We could do the same with our own classes.
Let’s implement a User
class and its mutable counterpart:
@interface User : NSObject <NSCopying, NSMutableCopying>
@property (nonnull, nonatomic, readonly) NSString *name;
- (nonnull instancetype)initWithUser:(nonnull User *)user;
@end
@interface MutableUser : User
@property (nullable, nonatomic) NSString *name;
@end
@interface User ()
@property (nonatomic) NSString *name;
@end
@implementation User
- (instancetype)initWithUser:(User *)user {
if (self = [super init]) {
_name = [user.name copy];
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
return [[User alloc] initWithUser:self];
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return [[MutableUser alloc] initWithUser:self];
}
@end
@implementation MutableUser
@dynamic name;
@end
Now we are able to use those classes the same way we use NSURLRequest
. However, there are several problems with this approach:
- Requires defensive copying to prevent accidental sharing of instances of
MutableUser
class where immutableUser
is expected - We had to use four lines of code for a single
name
property - We limited our ability to extend class hierarchy
Fortunately, there is an alternative way to create immutable objects which is an overlooked builder pattern. It is a very simple pattern that addresses all those problems.
Builder Pattern #
Let’s dive straight into implementation but this time we will start with a base class for our model objects - Entity
.
@class EntityBuilder;
@interface Entity : NSObject
@property (nonnull, nonatomic, readonly) NSString *ID;
- (nonnull instancetype)initWithBuilder:(nonnull EntityBuilder *)builder;
@end
@interface EntityBuilder : NSObject
@property (nullable, nonatomic) NSString *ID;
- (nonnull Entity *)build;
@end
@implementation Entity
- (instancetype)initWithBuilder:(EntityBuilder *)builder {
// You can also assers that validate builder here
if (self = [super init]) {
_ID = builder.ID;
}
return self;
}
@end
@implementation EntityBuilder
- (Entity *)build {
return [[Entity alloc] initWithBuilder:self];
}
@end
Entity
class has no mutable counterpart, defensive copying is no longer required. We also used just two lines of code for an ID
property.