NSOrderedSet
Here’s a question: why isn’t NSOrdered a subclass of NSSet?
It seems perfectly logical, after all, for NSOrdered–a class that enforces the same uniqueness constraint of NSSet–to be a subclass of NSSet. It has the same methods as NSSet, with the addition of some NSArray-style methods like object. By all accounts, it would seem to perfectly satisfy the requirements of the Liskov substitution principle, that:
If
Sis a subtype ofT, then objects of typeTin a program may be replaced with objects of typeSwithout altering any of the desirable properties of that program.
So why is NSOrdered a subclass of NSObject and not NSSet or even NSArray?
Mutable / Immutable Class Clusters
Class Clusters are a design pattern at the heart of the Foundation framework; the essence of Objective-C’s simplicity in everyday use.
But class clusters offer simplicity at the expense of extensibility, which becomes especially tricky when it comes to mutable / immutable class pairs like NSSet / NSMutable.
As expertly demonstrated by Tom Dalling in this Stack Overflow question, the method -mutable creates an inconsistency that is inherent to Objective-C’s constraint on single inheritance.
To start, let’s look at how -mutable is supposed to work in a class cluster:
let immutable = NSSet()
let mutable = immutable.mutable Copy() as! NSMutable Set
mutable.is Kind Of Class(NSSet.self) // true
mutable.is Kind Of Class(NSMutable Set.self) // true
NSSet* immutable = [NSSet set];
NSMutable Set* mutable = [immutable mutable Copy];
[mutable is Kind Of Class:[NSSet class]]; // YES
[mutable is Kind Of Class:[NSMutable Set class]]; // YES
Now let’s suppose that NSOrdered was indeed a subclass of NSSet:
// class NSOrdered Set: NSSet {...}
let immutable = NSOrdered Set()
let mutable = immutable.mutable Copy() as! NSMutable Ordered Set
mutable.is Kind Of Class(NSSet.self) // true
mutable.is Kind Of Class(NSMutable Set.self) // false (!)
// @interface NSOrdered Set : NSSet
NSOrdered Set* immutable = [NSOrdered Set ordered Set];
NSMutable Ordered Set* mutable = [immutable mutable Copy];
[mutable is Kind Of Class:[NSSet class]]; // YES
[mutable is Kind Of Class:[NSMutable Set class]]; // NO (!)
”
That’s no good… since NSMutable couldn’t be used as a method parameter of type NSMutable. So what happens if we make NSMutable a subclass of NSMutable as well?
// class NSOrdered Set: NSSet {...}
// class NSMutable Ordered Set: NSMutable Set {...}
let immutable = NSOrdered Set()
let mutable = immutable.mutable Copy() as! NSMutable Ordered Set
mutable.is Kind Of Class(NSSet.self) // true
mutable.is Kind Of Class(NSMutable Set.self) // true
mutable.is Kind Of Class(NSOrdered Set.self) // false (!)
// @interface NSOrdered Set : NSSet
// @interface NSMutable Ordered Set : NSMutable Set
NSOrdered Set* immutable = [NSOrdered Set ordered Set];
NSMutable Ordered Set* mutable = [immutable mutable Copy];
[mutable is Kind Of Class:[NSSet class]]; // YES
[mutable is Kind Of Class:[NSMutable Set class]]; // YES
[mutable is Kind Of Class:[NSOrdered Set class]]; // NO (!)
This is perhaps even worse, as now NSMutable couldn’t be used as a method parameter expecting an NSOrdered.
No matter how we approach it, we can’t stack a mutable / immutable class pair on top of another existing mutable / immutable class pair. It just won’t work in Objective-C.
Rather than subject ourselves to the perils of multiple inheritance, we could use Protocols to get us out of this pickle (as it does every other time the spectre of multiple inheritance is raised). Indeed, Foundation’s collection classes could become more aspect-oriented by adding protocols:
NSArray : NSObject <NSOrderedCollection> NSSet : NSObject <NSUniqueCollection> NSOrderedSet : NSObject <NSOrdered Collection, NSUnique Collection>
However, to reap any benefit from this arrangement, all of the existing APIs would have to be restructured to have parameters accept id <NSOrdered instead of NSArray. But the transition would be painful, and would likely open up a whole can of edge cases… which would mean that it would never be fully adopted… which would mean that there’s less incentive to adopt this approach when defining your own APIs… which are less fun to write because there’s now two incompatible ways to do something instead of one… which…
…wait, why would we use NSOrdered in the first place, anyway?
NSOrdered was introduced in iOS 5 & OS X Lion. The only APIs changed to add support for NSOrdered, though, were part of Core Data.
This was fantastic news for anyone using Core Data at the time, as it solved one of the long-standing annoyances of not having a way to arbitrarily order relationship collections. Previously, you’d have to add a position attribute, which would be re-calculated every time a collection was modified. There wasn’t a built-in way to validate that your collection positions were unique or that the sequence didn’t have any gaps.
In this way, NSOrdered is an answer to our prayers.
Unfortunately, its very existence in Foundation has created something between an attractive nuisance and a red herring for API designers.
Although it is perfectly suited to that one particular use case in Core Data, NSOrdered is probably not a great choice for the majority of APIs that could potentially use it. In cases where a simple collection of objects is passed as a parameter, a simple NSArray does the trick–even if there is an implicit understanding that you shouldn’t have duplicate entries. This is even more the case when order matters for a collection parameter–just use NSArray (there should be code to deal with duplicates in the implementation anyway). If uniqueness does matter, or the semantics of sets makes sense for a particular method, NSSet has and remains a great choice.
So, as a general rule: NSOrdered is useful for intermediary and internal representations, but you probably shouldn’t introduce it as a method parameters unless it’s particularly well-suited to the semantics of the data model.
If nothing else, NSOrdered illuminates some of the fascinating implications of Foundation’s use of the class cluster design pattern. In doing so, it allows us better understand the trade-off between simplicity and extensibility as we make these choices in our own application designs.