Chris Dzombak

sharing preview •

Cocoa’s mutable-subclass pattern is an antipattern

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 Apple’s Foundation framework?

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.