Classes are globals, too

Software engineers are used to the notion that global variables are a bad idea. Globals are usually accessed by asking, not by telling. They introduce tight coupling between any module that uses the global and the one that declares it, and (more dangerously) implicit coupling between all of the modules that use the global.

It can be hard to test code that uses globals. You typically need to supply a different object (by which I mean a linker object) that exports the same symbol, so that in tests you get to configure the global as you need it. Good luck if that’s deeply embedded in other code you do need, like NSApp.

While we’re on the subject of global variables, the names of classes are all in the global namespace. We know that: there is only one NSKeyedUnarchiver class, and everyone who uses the NSKeyedUnarchiver class gets the same behaviour. If any module that uses NSKeyedUnarchiver changes it (e.g. by swizzling a method on the metaclass), then every other client of NSKeyedUnarchiver gets the modified behaviour.

[If you want a different way of looking at this: Singletons are evil, and each class is the singleton instance of its metaclass, therefore class objects are evil.]

Now that makes it hard to test use of a class method: if I want to investigate how my code under test interacts with a class method, I have these options:

  • Swizzle the method. This is possible, but comparatively high effort and potentially complicated; should the swizzled method call through to the original or reimplement parts of it behaviour? What goes wrong if it doesn’t, and what needs to be supported if it does? Using the example of NSKeyedArchiver, if you needed to swizzle +archiveRootObject:toFile: you might need to ensure that the object graph still receives its -encodeWithCoder: messages – but then you might need to make sure that they send the correct data to the coder…
  • Tell the code under test what class to use, don’t let it ask. So you’d have a property on your object @property (nonatomic, strong) Class coderClass; and then you’d use the +[coderClass archiveRootObject:toFile:] method. In your app, you set the property to NSKeyedArchiver and in the tests, to whatever it is you need.

    That’s simple, and solves the problem, but rarely seen. I think this is mainly because there’s no way to tell the Objective-C compiler “this variable represents a Class that’s NSCoder or one of its subclasses”, so it’s hard to convince the type-safety mechanism that you know archiverClass responds to +archiveRootObject:toFile:.

  • Use instance methods instead of class methods, and tell the code you’re testing what instance to use. This is very similar to the above solution, but means that you pass a fully-configured object into the code under test rather than a generic(-ish) class reference. In your code you’d have a property @property (nonatomic, strong) NSCoder *coder in to -encodeRootObject: and rely on it having been configured correctly to know what to do as a result of that.

    This solution doesn’t suffer from the type-safety problem introduced in the previous solution; you told the compiler that you’re talking to an NSCoder (but not what specific type of NSCoder) so it knows you can do -encodeRootObject:.

Notice that even if you decide to go for the third solution here (the one I usually try to use) and use object instances instead of classes for all work, there’s one place where you must rely on a Class object: that’s to create instances of the class. Whether through “standard” messages like +alloc or +new, or conveniences like +arrayWithObjects:, you need a Class to create any other Objective-C object.

I’d like to turn the above observation into a guideline to reduce the proliferation of Class globals: an object’s class should only be used at the point of instantiation. Once you’ve made an object, pass it to the object that needs it, which should accept the most generic type available that declares the things that object needs. The generic type could be an abstract class like NSCoder or NSArray (notice that all array objects are created by the Foundation library, and application code never sees the specific class that was instantiated), or a protocol like UITableViewDataSource.

Now we’ve localised that pesky global variable to the smallest possible realm, where it can do the least damage.

About Graham

I make it faster and easier for you to create high-quality code.
This entry was posted in code-level, software-engineering, TDD. Bookmark the permalink.