Cocoa’s mutable-subclass pattern is an antipattern
Reading Brent Simmon’s latest post today, something occurred to me. Yes, we know we can’t mutate a mutable array while enumerating, and yes, we have to take care to avoid that. But wait a second—why is that our problem, as users of the Foundation framework?
It’s because the mutable-subclass pattern we’re all familiar with, used by NSArray/NSMutableArray
, NSDictionary/NSMutableDictionary
, and friends, violates a core object-oriented design principle: the Liskov substitution principle (aka. the “L” in SOLID).
The remainder of this post will use NS{Mutable}Array
as an example, but the problem here applies equally to most Cocoa classes that use the mutable-subclass pattern. Note also that this post isn’t about class clusters—that’s totally orthogonal to the principle I’ll discuss.
Informally, the Liskov substitution principle says that if B
is a subclass of A
, you must be able to use an instance of B
anywhere you might use an A
. Or to put another way, it’s just a fancy way to express the “is-a” relationship B
has to A
.
NS{Mutable}Array
and friends violate that principle.
NSArray
implements NSFastEnumeration
(and some other enumeration methods); part of its contract is that users may iterate over the collection. NSFastEnumeration
and friends don’t say anything about when users may iterate over the collection, or prohibit enumeration at any time.
Enter NSMutableArray
. It’s a subclass of NSArray
, so in principle it should be usable everywhere NSArray
is. But as Brent discusses in his post, that’s just not true: enumerating a mutable array is fraught with peril. Not so with a plain old NSArray
. (This is all the more dangerous because, since NSMutableArray
“is-a” NSArray
, methods that claim to return NSArray
are free to return an NSMutableArray
, opaque to you.)
Though NSMutableArray
conforms to NSFastEnumeration
, it doesn’t fulfill the same contract as its superclass, and thus it violates the Liskov substitution principle.
Let’s consider a possible solution where NSArray
and NSMutableArray
do not have a superclass/subclass relationship. NSArray
could just as well be implemented using an NSMutableArray
under the hood, exposing only the methods that don’t modify the array, and adding enumeration to its interface. (One imagines that an NSArray
protocol would come into play to allow the common, getter methods to be shared in a single interface, leaving the NSArray
class to implement safe enumeration and NSMutableArray
to add mutation.)
This wouldn’t be much more cumbersome than the current situation in Cocoa, where we’re constantly calling -mutableCopy
and -copy
to balance our needs for mutability and safety, and it would be safer.
But that’s not the case. Instead, we have a programming environment where we can’t even trust our type system. This is a sad, dangerous, and (in the case of Cocoa) unfixable situation. Learn from it; in your own programming, moving forward, avoid the mutable-subclass pattern.
It’s worth noting that Swift avoids this problem nicely; its mutation semantics for value types provide a much more elegant way to handle mutable data structures. And the new-in-Swift-1.2 isUniquelyReferenced
function allows you to build safe, efficient data structures on par with the standard library’s collection types.