Like all C-based languages, Objective-C files usually come in pairs: there’s a header file, and an implementation file. Either can use the #import directive to include other header files. And if you’re not careful, it’s easy to create an explosion of file dependencies. What are the consequences? How do we tame #import dependencies?[This post is part of the Code Smells in Objective-C series.]
Unnecessary #imports in a .m file are a nuisance. Why? Because it forces you to have those other files in your project. This isn’t a big deal when you’re working on a single project, but immediately causes trouble when you start a new project and want to reuse some source files.
But unnecessary #imports in a .h file are even worse: the problem grows exponentially! That’s because a header imports another header, which imports another header, and so on. Think of it as a dependency graph:
Say A.h imports B.h and C.h. But B.h also imports D.h. So to add A to your project, you have to pull in B, C and D as well. And this graph is about as simple as it comes. If unnecessary #imports aren’t kept pruned away, the dependency graph will get out of hand.
File dependencies also affect incremental builds. Touching D.h causes Xcode to rebuild D.m, B.m and A.m. If you’ve only worked on small projects, it’s no big deal. But trust me on this: on a large project, things can bog down. I’ve had people tell me, “This isn’t important, I need to take a short break anyway.” But folks who say that aren’t doing test-driven development. In TDD, unit tests give feedback about the code you just changed. The more you can tighten that feedback loop, the more you can stay “in the zone.” Even a few seconds can make a difference.
While untamed #imports in header files affect build time, don’t think that implementation files are off the hook! The dependency graph is still at work, though in a less obvious way.
Let’s refer to the same graph, but change things around a bit. Say A.m imports B.h and C.h. But B.m imports D.h. The problem here isn’t that touching D causes too many modules to recompile. The problem is that to include A in your project, you have to drag along B, C and D as well. You can scan A.m to find the first level of file dependencies by reading its #import directives. But the dependency on D is hidden. You won’t discover it until you add B, and the build fails.
Trying to add a single module A can quickly become an exercise in frustration, as you chase down successive levels of the dependency graph.
So let’s look at how to tame file dependencies, first in header files, then in implementation files. Starting with header files, the code smell to look for is simply: too many #imports. Let’s consider which #imports are necessary, and which we can avoid.
Say we’re defining a class Foo. It inherits from Superclass, and implements two protocols:
It’s necessary to #import the headers that define Superclass, Protocol1 and Protocol2.
But what about objects that are instance variables or properties? What about other protocols? What about objects passed as arguments, or returned by methods? Let’s fill in the contents of Foo’s declaration:
We’ve added references to Bar, DelegateProtocol, Baz and Qux. How many declarations do we need to #import for all these? Answer: None! All we need is to forward-declare them before the @interface:
Some like to combine all @class forward declarations on one line, but I prefer one per line. It lets me sort them, which in turn helps me find any duplicates. Also, doing one declaration per line reveals just how many there are.
Note: For classes from built-in frameworks such as UIKit, just #import the framework and don’t bother forward-declaring each class. A framework comes as a single prebuilt chunk with a master header, so it doesn’t affect file dependencies at the same granular level. This is a good rule to follow for any frameworks and libraries, unless you create a particular library as part of your build process.
…Getting back to our example, the only headers we need to #import are those that declare the superclass we’re inheriting, and the protocols we’re implementing:
There may be other non-object declarations we need to bring in, such as enums and typedefs, but as a general rule, having any other #imports in a header file is a code smell.
This is also why I isolate protocol declarations in their own headers instead of lumping them in with the classes they cooperate with. It keeps the dependency graph pruned.
Forward declarations are infrequent in implementation files, because we’re usually sending messages to objects, not just passing objects around. (Though if your class is the middle-man of a delegation, you will find times when a method takes an argument from a return value and passes it back as its own return value. Then see if you can use forward declaration and avoid the #import.)
So we usually can’t use forward declaration to trim #imports in the .m file. But in both .h and .m files, #imports tend to accumulate over time. It’s not hard to have #imports that are simply unnecessary and can be deleted outright. This happens when:
Basically, it’s cruft management. An occasional clean-out of crufty #imports can trim unnecessary file dependencies. In a coming post on #import completeness (the opposite of having too many), I’ll share why #import order matters.
But even if you drop all unnecessary #imports, you can still end up with one #import after another in a long list. In the heat of development, it’s easy to lump more and more stuff into a class. Cohesion goes down (because the class is doing too many things), and coupling increases. The result is a horrible dependency graph.
In Martin Fowler’s Refactoring book, he describes a code smell called Large Class, where the indicator is too many instance variables. I’d say too many #imports is another indicator of the Large Class smell. (It follows that too many forward declarations is also an indicator.) Follow the Large Class recommendation: use the Extract Class or Extract Subclass refactoring steps to break things up. You’ll be pleasantly surprised at the difference! “High cohesion” will change from a theory, to something you can actually feel.
Let’s bring it all home! Here are the things to look for to manage file dependencies:
OK, go check your code! I’m going to check my own code, because I know there are things I’ve missed. Let’s tame those wild file dependencies!
Question: What discoveries did you make as you examined your code’s file dependencies? Leave a comment below.[This post is part of the Code Smells in Objective-C series.]
Jon is a coach and consultant on iOS Clean Code (Test Driven Development, unit testing, refactoring, design). He’s been practicing TDD since 2001. You can learn more about his background, or see what services he can bring to your organization.