6

Is there a standard pattern for implementing a mutable/immutable object class pair in Objective-C? I currently have something like the following, which I wrote based off this link

Immutable Class:

@interface MyObject : NSObject <NSMutableCopying> {
    NSString *_value;
}

@property (nonatomic, readonly, strong) NSString *value;
- (instancetype)initWithValue:(NSString *)value;

@end

@implementation MyObject
@synthesize value = _value;
- (instancetype)initWithValue:(NSString *)value {
    self = [self init];
    if (self) {
        _value = value;
    }
    return self;
}


- (id)mutableCopyWithZone:(NSZone *)zone {
    return [[MyMutableObject allocWithZone:zone] initWithValue:self.value];
}

@end

Mutable Class:

@interface MyMutableObject : MyObject
@property (nonatomic, readwrite, strong) NSString *value;
@end


@implementation MyMutableObject
@dynamic value;

- (void)setValue:(NSString *)value {
    _value = value;
}

@end

This works, but it exposes the iVar. Is there a better implementation that remedies this situation?

wL_
  • 997
  • 8
  • 13
  • Note that iOS has several different implementations of NSArray, for example, and some of them are (temporarily) mutable -- the object is created, it's populated (with separate operations), and only then is the object marked immutable. Exactly how they do this is unclear, though -- it's part of the great mystery of class clusters. – Hot Licks Aug 22 '14 at 00:16
  • 2
    Your implementation is the "standard" pattern however it is not what Apple actually uses for most of their mutable classes. They mostly use class clusters as @HotLicks talked about which are extremely complicated and you almost certainly do not want to go down that road, unless you're facing serious performance problems (as some people do with NSString and NSArray). Beware that if self.value was a mutable class, you would have to make a copy of it inside `mutableCopyWithZone:`. – Abhi Beckert Aug 22 '14 at 01:29
  • My question is, are you looking to provide mutable and immutable versions of your class because you want there to be a distinction between the two as part of an API, or do you want to be able to modify your—otherwise immutable—object internally without accessing instance variables? If it's the first, consider why that would be necessary. If it really is, class clusters are an option, but they're not a great design pattern for unless it's really necessary, as @AbhiBeckert mentioned. – Itai Ferber Aug 22 '14 at 01:38
  • @HotLicks That's not _quite_ how `NS{Mutable}Array` works (believe me, however complicated you think the implementation is, it's at least 100x more complicated and gross, but damn it's fast), but I think you've captured a little bit of the ugliness of class clusters. There's really no good way to make them pretty (unless your class is somewhat trivial, and the majority of the implementation doesn't change between the mutable and immutable versions). – Itai Ferber Aug 22 '14 at 02:00
  • @ItaiFerber - Yeah, like sausages and laws, NS{Mutable} objects are something you should not watch being made. – Hot Licks Aug 22 '14 at 02:20

3 Answers3

4

Your solution follows a very good pattern: the mutable class does not duplicate anything from its base, and exposes an additional functionality without storing any additional state.

This works, but it exposes the iVar.

Due to the fact that instance variables are @protected by default, the exposed _value is visible only to the classes inheriting MyObject. This is a good tradeoff, because it helps you avoid data duplication without publicly exposing the data member used for storing the state of the object.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
0

Is there a better implementation that remedies this situation?

Declare the value property in a class extension. An extension is like a category without a name, but must be part of the class implementation. In your MyMutableObject.m file, do this:

@interface MyMutableObject ()
@property(nonatomic, readwrite, strong) value
@end

Now you've declared your property, but it's only visible inside your implementation.

Caleb
  • 124,013
  • 19
  • 183
  • 272
0

The answer from dasblinkenlight is correct. The pattern provided in the question is fine. I provide an alternative that differs in two ways. First, at the expense of an unused iVar in the mutable class, the property is atomic. Second, as with many foundation classes, a copy of an immutable instance simply returns self.

MyObject.h:

@interface MyObject : NSObject <NSCopying, NSMutableCopying>

@property (atomic, readonly, copy) NSString *value;

- (instancetype)initWithValue:(NSString *)value NS_DESIGNATED_INITIALIZER;

@end

MyObject.m

#import "MyObject.h"
#import "MyMutableObject.h"

@implementation MyObject

- (instancetype)init {
    return [self initWithValue:nil];
}

- (instancetype)initWithValue:(NSString *)value {
    self = [super init];
    if (self) {
        _value = [value copy];
    }
    return self;
}

- (id)copyWithZone:(NSZone *)zone {
    return self;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
    // Do not use the iVar here or anywhere else.
    // This pattern requires always using self.value instead of _value (except in the initializer).
    return [[MyMutableObject allocWithZone:zone] initWithValue:self.value];
}

@end

MyMutableObject.h:

#import "MyObject.h"

@interface MyMutableObject : MyObject

@property (atomic, copy) NSString *value;

@end

MyMutableObject.m:

#import "MyMutableObject.h"

@implementation MyMutableObject

@synthesize value = _value; // This is not the same iVar as in the superclass.

- (instancetype)initWithValue:(NSString *)value {
    // Pass nil in order to not use the iVar in the parent.
    // This is reasonably safe because this method has been declared with NS_DESIGNATED_INITIALIZER.
    self = [super initWithValue:nil];
    if (self) {
        _value = [value copy];
    }
    return self;
}

- (id)copyWithZone:(NSZone *)zone {
    // The mutable class really does need to copy, unlike super.
    return [[MyObject allocWithZone:zone] initWithValue:self.value];
}

@end

A fragment of test code:

NSMutableString *string = [NSMutableString stringWithString:@"one"];
MyObject *object = [[MyObject alloc] initWithValue:string];
[string appendString:@" two"];
NSLog(@"object: %@", object.value);
MyObject *other = [object copy];
NSAssert(object == other, @"These should be identical.");
MyMutableObject *mutable1 = [object mutableCopy];
mutable1.value = string;
[string appendString:@" three"];
NSLog(@"object: %@", object.value);
NSLog(@"mutable: %@", mutable1.value);

Some debugging right after the last line above:

2017-12-15 21:51:20.800641-0500 MyApp[6855:2709614] object: one
2017-12-15 21:51:20.801423-0500 MyApp[6855:2709614] object: one
2017-12-15 21:51:20.801515-0500 MyApp[6855:2709614] mutable: one two
(lldb) po mutable1->_value
one two

(lldb) po ((MyObject *)mutable1)->_value
 nil

As mentioned in the comments this requires discipline in the base class to use the getter instead of the iVar. Many would consider that a good thing, but that debate is off-topic here.

A minor difference you might notice is that I have used the copy attribute for the property. This could be made strong instead with very little change to the code.

Rik Renich
  • 774
  • 6
  • 12