Skip to content

Instantly share code, notes, and snippets.

@victorpimentel
Last active October 9, 2015 16:11
Show Gist options
  • Save victorpimentel/41f2f0ddec132b14606d to your computer and use it in GitHub Desktop.
Save victorpimentel/41f2f0ddec132b14606d to your computer and use it in GitHub Desktop.
Immutable & mutable models
@interface TMUser1 : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy, readonly) NSString *userId;
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *surname;
@property (nonatomic, readonly) NSURL *avatarURL;
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL;
@end
@interface TMMutableUser1 : TMUser1
@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *surname;
@property (nonatomic) NSURL *avatarURL;
@end
@interface TMUser1 ()
@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *surname;
@property (nonatomic) NSURL *avatarURL;
@end
@implementation TMUser1
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL
{
self = [super init];
if (self)
{
_userId = [userId copy];
_name = [name copy];
_surname = [surname copy];
_avatarURL = avatarURL;
}
return self;
}
- (TMUser1 *)copyWithZone:(NSZone *)zone
{
return self;
}
- (TMMutableUser1 *)mutableCopyWithZone:(NSZone *)zone
{
return [[TMMutableUser1 alloc] initWithUserId:self.userId
name:self.name
surname:self.surname
avatarURL:self.avatarURL];
}
@end
@implementation TMMutableUser1
@dynamic userId;
@dynamic name;
@dynamic surname;
@dynamic avatarURL;
- (TMUser1 *)copyWithZone:(NSZone *)zone
{
return [[TMUser1 alloc] initWithUserId:self.userId
name:self.name
surname:self.surname
avatarURL:self.avatarURL];
}
@end
@interface TMUser2 : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy, readonly) NSString *userId;
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *surname;
@property (nonatomic, readonly) NSURL *avatarURL;
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL;
@end
@protocol TMMutableUser2 <NSObject>
@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *surname;
@property (nonatomic) NSURL *avatarURL;
@end
typedef TMUser2<TMMutableUser2> TMMutableUser2;
@interface TMUser2 ()
@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *surname;
@property (nonatomic) NSURL *avatarURL;
@end
@implementation TMUser2
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL
{
self = [super init];
if (self)
{
_userId = [userId copy];
_name = [name copy];
_surname = [surname copy];
_avatarURL = avatarURL;
}
return self;
}
- (TMUser2 *)copyWithZone:(NSZone *)zone
{
return [[TMUser2 alloc] initWithUserId:self.userId
name:self.name
surname:self.surname
avatarURL:self.avatarURL];
}
- (TMMutableUser2 *)mutableCopyWithZone:(NSZone *)zone
{
return [self copyWithZone:zone];
}
@end
@interface TMUser3 : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy, readonly) NSString *userId;
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *surname;
@property (nonatomic, readonly) NSURL *avatarURL;
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL;
@end
@interface TMMutableUser3 : TMUser3
@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *surname;
@property (nonatomic) NSURL *avatarURL;
@end
@implementation TMUser3
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL
{
self = [super init];
if (self)
{
_userId = [userId copy];
_name = [name copy];
_surname = [surname copy];
_avatarURL = avatarURL;
}
return self;
}
- (TMUser3 *)copyWithZone:(NSZone *)zone
{
return self;
}
- (TMMutableUser3 *)mutableCopyWithZone:(NSZone *)zone
{
return [[TMMutableUser3 alloc] initWithUserId:self.userId
name:self.name
surname:self.surname
avatarURL:self.avatarURL];
}
@end
@interface TMMutableUser3 ()
@property (nonatomic, copy) NSString *mutableUserId;
@property (nonatomic, copy) NSString *mutableName;
@property (nonatomic, copy) NSString *mutableSurname;
@property (nonatomic, copy) NSURL *mutableAvatarURL;
@end
@implementation TMMutableUser3
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL
{
self = [super init];
if (self)
{
_mutableUserId = [userId copy];
_mutableName = [name copy];
_mutableSurname = [surname copy];
_mutableAvatarURL = avatarURL;
}
return self;
}
- (TMUser1 *)copyWithZone:(NSZone *)zone
{
return [[TMUser1 alloc] initWithUserId:self.userId
name:self.name
surname:self.surname
avatarURL:self.avatarURL];
}
- (NSString *)userId
{
return self.mutableUserId;
}
- (void)setUserId:(NSString *)userId
{
self.mutableUserId = userId;
}
- (NSString *)name
{
return self.mutableName;
}
- (void)setName:(NSString *)name
{
self.mutableName = name;
}
- (NSString *)surname
{
return self.mutableSurname;
}
- (void)setSurname:(NSString *)surname
{
self.mutableSurname = surname;
}
- (NSURL *)avatarURL
{
return self.mutableAvatarURL;
}
- (void)setAvatarURL:(NSURL *)avatarURL
{
self.mutableAvatarURL = avatarURL;
}
@end
@interface TMUser4 : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy, readonly) NSString *userId;
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *surname;
@property (nonatomic, readonly) NSURL *avatarURL;
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL;
@end
@interface TMMutableUser4 : TMUser4
@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *surname;
@property (nonatomic) NSURL *avatarURL;
@end
@interface TMUser4 ()
{
@protected
NSString *_userId;
NSString *_name;
NSString *_surname;
NSURL *_avatarURL;
}
@end
@implementation TMUser4
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL
{
self = [super init];
if (self)
{
_userId = [userId copy];
_name = [name copy];
_surname = [surname copy];
_avatarURL = avatarURL;
}
return self;
}
- (TMUser4 *)copyWithZone:(NSZone *)zone
{
return self;
}
- (TMMutableUser4 *)mutableCopyWithZone:(NSZone *)zone
{
return [[TMMutableUser4 alloc] initWithUserId:self.userId
name:self.name
surname:self.surname
avatarURL:self.avatarURL];
}
@end
@implementation TMMutableUser4
@dynamic userId;
@dynamic name;
@dynamic surname;
@dynamic avatarURL;
- (TMUser4 *)copyWithZone:(NSZone *)zone
{
return [[TMUser4 alloc] initWithUserId:self.userId
name:self.name
surname:self.surname
avatarURL:self.avatarURL];
}
- (void)setUserId:(NSString *)userId
{
_userId = [userId copy];
}
- (void)setName:(NSString *)name
{
_name = [name copy];
}
- (void)setSurname:(NSString *)surname
{
_surname = [surname copy];
}
- (void)setAvatarURL:(NSURL *)avatarURL
{
_avatarURL = avatarURL;
}
@end
@protocol TMUser5 <NSCopying, NSMutableCopying>
@property (nonatomic, copy, readonly) NSString *userId;
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *surname;
@property (nonatomic, readonly) NSURL *avatarURL;
@end
@interface TMMutableUser5 : NSObject <TMUser5>
@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *surname;
@property (nonatomic, readonly) NSURL *avatarURL;
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL;
@end
typedef NSObject<TMUser5> TMUser5;
@implementation TMMutableUser5
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL
{
self = [super init];
if (self)
{
_userId = [userId copy];
_name = [name copy];
_surname = [surname copy];
_avatarURL = avatarURL;
}
return self;
}
- (id<TMUser5>)copyWithZone:(NSZone *)zone
{
return [[TMMutableUser5 alloc] initWithUserId:self.userId
name:self.name
surname:self.surname
avatarURL:self.avatarURL];
}
- (TMMutableUser5 *)mutableCopyWithZone:(NSZone *)zone
{
return [self copyWithZone:zone];
}
@end
@interface TMUser6 (Mutability)
@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *surname;
@property (nonatomic) NSURL *avatarURL;
@end
@interface TMUser6 : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy, readonly) NSString *userId;
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *surname;
@property (nonatomic, readonly) NSURL *avatarURL;
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL;
@end
@interface TMUser6 ()
@property (nonatomic, copy) NSString *userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *surname;
@property (nonatomic) NSURL *avatarURL;
@end
@implementation TMUser6
- (instancetype)initWithUserId:(NSString *)userId
name:(NSString *)name
surname:(NSString *)surname
avatarURL:(NSURL *)avatarURL
{
self = [super init];
if (self)
{
_userId = [userId copy];
_name = [name copy];
_surname = [surname copy];
_avatarURL = avatarURL;
}
return self;
}
- (TMUser6 *)copyWithZone:(NSZone *)zone
{
return [[TMUser6 alloc] initWithUserId:self.userId
name:self.name
surname:self.surname
avatarURL:self.avatarURL];
}
- (TMUser6 *)mutableCopyWithZone:(NSZone *)zone
{
return [self copyWithZone:zone];
}
@end
@victorpimentel
Copy link
Author

With all these versions you can do this (verbatim, even with the ones that use protocols):

TMUser *originalUser = [[TMUser alloc] initWithUserId:@"userId" name:@"one" surname:nil avatarURL:nil];
TMMutableUser *mutableUser = [originalUser mutableCopy];
mutableUser.name = @"two";
TMUser *copiedUser = [mutableUser copy];

NSAssert([originalUser.name isEqualToString:@"one"]);
NSAssert([mutableUser.name isEqualToString:@"two"]);
NSAssert([copiedUser.name isEqualToString:@"two"]);
NSAssert(originalUser != mutableUser);
NSAssert(mutableUser != copiedUser);

Verbosity ranking (Lines of code in .m)

  • Option 5: 32
  • Option 2: 41
  • Option 6: 41 (+8 lines from the private category)
  • Option 1: 58
  • Option 4: 80
  • Option 3: 109

Safety ranking

  • Option 3: Cannot mutate immutable properties, not even the mutable child in the implementation file.
  • Option 4: Cannot mutate immutable properties, but ivars are shared between parent and the child so the child can mutate them.
  • Option 1, 2 and 5: Can always mutate "immutable" copies just by casting it.
  • Option 6: Can always mutate "immutable" copies just by adding an import.

Efficiency ranking

  • Option 1: Copies are only done when strictly needed and ivars are shared.
  • Option 2, 5 and 6: Copies are done even when it's not needed, but ivars are shared.
  • Option 4: Copies are only done when strictly needed, but mutable copies have twice extra ivars and redirections when setting properties..
  • Option 3: Copies are only done when strictly needed, but mutable copies have twice extra ivars and redirections when setting/accessing properties.

"Prone to errors" ranking (kind of subjective)

  • Option 5: Less code so less errors are possible. But you do need to enforce the use of the protocol through the app (contacts do this now).
  • Option 1 and 2: You can remove or forget the properties in the implementation file, and the compiler will not tell you (but it will crash).
  • Option 4: You need to remember to add every setter, and the compiler will not tell you if you don't implement it (but it will crash in runtime).
  • Option 3: Really dangerous, since you not only need to remember to write the setter but also the getter. And if you forget the getter you will get weird weird weird inconsistencies (like writing a value and reading another value).
  • Option 6: Well, we know that it is almost impossible to enforce a good use of a private category, it will be used all over the place.

Usage ranking

  • Option 1, 3 and 4. They are kind of easy to deal with, just classes like NSArray/NSMutableArray.
  • Option 6: Based on lies, you don't even need "mutableCopy", everything is mutable.
  • Option 5. Use of protocol for read only models, classes for read/write models. You cannot do [[TMUser alloc] init].
  • Option 2. Use of protocol for read/write models, classes for read only models. You cannot do [[TMMutableUser alloc] init].

Implementation weirdness ranking (hard to understand)

  • Option 5: Use of protocols/typedef.
  • Option 2: Use of protocols/typedef, but a bit weird, also uses a class extension.
  • Option 1: Use of dynamic, also uses a class extension.
  • Option 6: Use of private category in a separate file.
  • Option 3: Custom setters/getters and twice the set of properties.
  • Option 4: Use of dynamic, custom setters and protected ivars.

Testing ranking

  • Option 5: Uses protocols, so it should be easier to mock those models (not that we should).
  • Option 1, 2, 3 and 4: Uses classes, it is easy to mock them but less easy than protocols.
  • Option 6: Who knows.

@cesteban
Copy link

cesteban commented Oct 9, 2015

There is one more solution not listed here, which is not having mutable objects at all.

@interface TMUser1 : NSObject

@property (nonatomic, copy, readonly) NSString *userId;
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *surname;
@property (nonatomic, readonly) NSURL *avatarURL;

- (instancetype)initWithUserId:(NSString *)userId
                          name:(NSString *)name
                       surname:(NSString *)surname
                     avatarURL:(NSURL *)avatarURL;

- (instancetype)userByMutatingUserId:(NSString *)userId;
- (instancetype)userByMutatingName:(NSString *) name;
- (instancetype)userByMutatingSurname:(NSString *) surname;
- (instancetype)userByMutatingAvatarURL:(NSURL *) avatarURL;

@end

@implementation TMUser6

- (instancetype)initWithUserId:(NSString *)userId
                          name:(NSString *)name
                       surname:(NSString *)surname
                     avatarURL:(NSURL *)avatarURL
{
    self = [super init];
    if (self)
    {
        _userId = [userId copy];
        _name = [name copy];
        _surname = [surname copy];
        _avatarURL = avatarURL;
    }
    return self;
}

- (instancetype)userByMutatingUserId:(NSString *)userId
{
    return [[[self class] alloc] initWithUserId:userId
                                           name:self.name
                                        surname:self.surname
                                      avatarURL:self.avatarURL];
}

- (instancetype)userByMutatingName:(NSString *)name
{
    return [[[self class] alloc] initWithUserId:self.userId
                                           name:name
                                        surname:self.surname
                                      avatarURL:self.avatarURL];
}

- (instancetype)userByMutatingSurname:(NSString *)surname
{
    return [[[self class] alloc] initWithUserId:self.userId
                                           name:self.name
                                        surname:surname
                                      avatarURL:self.avatarURL];
}

- (instancetype)userByMutatingAvatarURL:(NSURL *)avatarURL
{
    return [[[self class] alloc] initWithUserId:self.userId
                                           name:self.name
                                        surname:self.surname
                                      avatarURL:avatarURL];
}

@end

How this approach ranks in your categories:

  1. Verbosity: 68 lines, very verbose, but not the worst option.
  2. Safety ranking: Safe, there is no way to mutate an object, which was the whole point of this!
  3. Efficiency: Always require a copy. Not efficient compared with other approaches.
  4. "Prone to errors": Very safe, really difficult to screw up IMHO (other than really stupid copy&pasting errors, trivial to detect and fix).
  5. Usage ranking: Same usage we already know from many Cocoa casses (see for example NSURL and its methods family URLByAppendingPathComponent:).
  6. Implementation weirdness ranking: I don't see any weirdness in this approach, just objects.
  7. Testing ranking: In tests you probably would like to write Categories if you need to mutate objects very often... But you can just create them and threat them as immutable objects, which is precisely what they are.

This option could be painful for big objects, but none of the others is free from writting stupid boilerplate. And with this approach we have real proper immutable objects, instead of 'workarounds'.

My preference order would be this option first, and if not, then the option #2, which is very weird, and hacky, and murky, but at least is the easiest and shortest. And when it comes to mutable vs. immutable, there is nothing in between. There is nothing like 'half mutable'. Every approach in the list is making objects mutable in one way or another. So if we choose to mutate, let's mutate with the simplest possible approach.

@victorpimentel
Copy link
Author

Yeah, @cesteban, I wrote the ones that kept the NSCopying/NSMutableCopying interface, because I thought that we already voted for it in the arch committee after testing it in the field.

However we can decide it again if we see that other option with a different API will be better for us. That can open a can of worms too :P because then we have another option: to just publish the initialize and that's just it. TMPhoneNumber follows that option, and it has resulted in an explosion of initializers :/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment