Search code examples
objective-cimmutabilitynscopyingmutability

Correct pattern for mutable vs immutable


I'm wondering what the correct pattern for implementing Mutable vs Immutable data structures would be. I understand the concept and how it works, but how should I implement if using an underlying Cocoa data structure? I mean, if I use a NSSet, for instance. Lets say I have the following:

// MyDataStructure.h
@interface MyDataStructure : NSObject
@property (nonatomic, strong, readonly) NSSet * mySet;
@end


// MyDataStructure.m
@interface MyDataStructure ()
@property (nonatomic, strong) NSMutableSet * myMutableSet;
@end

@implementation MyDataStructure

- (NSSet *)mySet
{
    return [_myMutableSet copy];
}

@end

The only reason I'm using a mutable set as the underlying data structure,is so that the mutable version of this class can tamper with it. MyDataStructure per se does not really need a mutable set. Therefore, assuming that I have implemented some initialisers to make this class useful, here's how MyMutableDataStructure looks like:

// MyDataStructure.h (same .h as before)
@interface MyMutableDataStructure : MyDataStructure

- (void)addObject:(id)object;

@end

// MyDataStructure.m (same .m as before)
@implementation MyMutableDataStructure

- (void)addObject:(id)object
{
    [self.myMutableSet addObject:object];
}

@end

By using this pattern the underlying data structure is always mutable, and its immutable version is just an immutable copy (or is it??).

This also begs another question that arises when implementing the NSCopying protocol. Here's a sample implementation:

- (id)copyWithZone:(NSZone *)zone
{
    MyDataStructure * copy = [MyDataStructure allocWithZone:zone];
    copy->_myMutableSet = [_myMutableSet copyWithZone:zone];

    return copy;
}

Doesn't copyWithZone: return an immutable copy if that applies? So I'm basically assigning a NSSet instead to a NSMutableSet property, isn't that right?

Edit: While diving deeper into the issue I found some more issues surrounding this concern.

  1. mySet should be copy instead of strong.
  2. My copyWithZone: implementation isn't right either. I didn't mention it in the first post but that implementation relates to the Immutable version of the data structure (MyDataStructure). As I've read, Immutable data structures don't actually create a copy, they just return themselves. That makes sense.
  3. Because of 2., I needed to override copyWithZone: in the Mutable version (MyMutableDataStructure).

To make things clear:

// MyDataStructure.h
@property (nonatomic, copy, readonly) NSSet * mySet;

And

// MyDataStructure.m
@implementation MyDataStructure

- (id)copyWithZone:(NSZone *)zone
{
    // We don't really need a copy, it's Immutable
    return self;
}

- (id)mutableCopyWithZone:(NSZone *)zone
{
    // I also implement -mutableCopyWithZone:, in which case an actual (mutable) copy is returned
    MyDataStructure * copy = [MyMutableDataStructure allocWithZone:zone];
    copy-> _myMutableSet = [_myMutableSet mutableCopyWithZone:zone];

    return copy;
}

@end

@implementation MyMutableDataStructure

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

@end

It seems tricky at first, but I think I'm getting the hang of it. So the remaining questions are:

  1. Is the pattern correct?
  2. Does the getter for mySet return a mutable or immutable instance?
  3. (not listed before) Do I really need the copy signal when using the copy property attribute?

I appreciate your patience to read this far. Best.


Solution

  • Apple is the way

    All across Apple's libraries the pattern that is used is, the mutable version of the class can be created through -mutableCopy, or (let's say the class is called NSSomething), then it can be created through the methods -initWithSomething:(NSSomething*)something or +somethingWithSomething:(NSSomething*)something. NSMutableSomething always inherits from NSSomething and so the constructor methods are the same. (i.e., +[NSArray arrayWithArray:] and +[NSMutableArray arrayWithArray:] return their respective instance types, also you pass in a mutable object to make an immutable copy, aka [NSArray arrayWithArray:someNSMutableArrayObject])

    So this is how I would do it:

    Interface

    MyDataStructure.h

    // MyDataStructure.h
    @interface MyDataStructure : NSObject
    @property (nonatomic, strong) NSSet * mySet;
    + (instancetype)dataStructureWithDataStructure:(MyDataStructure*)dataStructure;
    - (instancetype)initWithDataStructure:(MyDataStructure*)dataStructure;
    @end
    

    MyMutableDataStructure.h

    // MyMutableDataStructure.h
    #import "MyDataStructure.h"
    @interface MyMutableDataStructure : MyDataStructure
    @property (nonatomic, strong) NSMutableSet * mySet; // Only needs to redefine this property.  The instantiation methods will be borrowed from the immutable class.
    @end;
    

    Implementation

    MyDataStructure.m

    @implementation MyDataStructure
    
    + (instancetype)dataStructureWithDataStructure:(MyDataStructure *)dataStructure {
        return [[self alloc] initWithDataStructure:dataStructure];
    }
    
    - (instancetype)initWithDataStructure:(MyDataStructure *)dataStructure {
        self = [super init];
        if (self) {
            self.mySet = [NSSet setWithSet:dataStructure.mySet];
        }
        return self;
    }
    
    - (instancetype)mutableCopy {
        return [MyMutableDataStructure dataStructureWithDataStructure:self];
    }    
    
    @end
    

    MyMutableDataStructure.m

    @implementation MyMutableDataStructure
    
    - (instancetype)initWithDataStructure:(MyDataStructure *)dataStructure {
        self = [super init];
        if (self) {
            self.mySet = [NSMutableSet setWithSet:dataStructure.mySet];
        }
        return self;
    }
    
    @end
    

    By using this pattern the underlying data structure is always mutable, and its immutable version is just an immutable copy (or is it??).

    No. To cut down on memory footprint, immutable objects do not need the same overhead as their mutable counterparts.

    copyWithZone

    Just make sure you use [self class] so MyMutableDataStructure can inherit this method and return its own type, and also don't forget the call to -init after +allocWithZone:

    __typeof(self) just declares the variable "copy" as whatever type self is, so it's completely inheritable by the mutable subclass.

    - (id)copyWithZone:(NSZone *)zone
    {
        __typeof(self) copy = [[[self class] allocWithZone:zone] init]; // don't forget init!
        copy.mySet = [self.mySet copyWithZone:zone];
        return copy;
    }
    

    ^ That method goes into the implementation of MyDataStructure

    In your original implementation,

    // We don't really need a copy, it's Immutable

    While this may be true for your project, it's an abuse of the naming convention. A method that starts with -copy should return a copy of the object.

    Going a little off topic:

    I want to touch on some things I saw in your original question...The first is about hiding a "mutable object" with an "immutable object" pointer reference. Maybe you need this functionality too, so here is a more robust way to do it and why.

    MyClass.h (notice no ownership attributes - because it's only really an alias - aka we only need this for thy synthesis of setters and getters)

    @property (nonatomic) NSSet *mySet;
    

    MyClass.m

    @interface MyClass () {
        NSMutableSet *_myMutableSet;
    }
    @implementation MyClass
    
    // returns a copy of the internal mutable set as an NSSet
    - (NSSet*)mySet {
        return [NSSet setWithSet:_myMutableSet];
    }
    
    // setter saves the internal mutable set as a copy of whatever set you hand it
    - (NSSet*)setMySet:(NSSet*)mySet {
        _myMutableSet = [NSMutableSet setWithSet:mySet];
    }
    

    I defined _myMutableSet as an ivar, to protect the getters and setters further. In your original code you put @property (...) myMutableSet in an interface extension in the .m file. This synthesizes getters and setters automatically, so even if the declaration is apparently "private", one could call [myDataStructure performSelector:@selector(setMutableSet:) withObject:someMutableSet]; and it will work despite the "Undeclared selector" compiler warning.

    Also, in your original implementation of -mySet:

    - (NSSet *)mySet {
        return [_myMutableSet copy];
    }
    

    This returns a copy of a pointer to _myMutableSet, type-casted as an NSSet*. Therefore if someone re-casted it as NSMutableSet* they could alter the underlying mutable set.