#imports Gone Wild! How to Tame File Dependencies

November 6, 2011 — 15 Comments

#imports (not Girls) Gone Wild

[This post is part of the Code Smells in Objective-C series.]

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?

File dependencies

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:

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.

Problem: Incremental build time

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.

Problem: Hidden dependencies

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.

Code smell: Too many #imports in .h

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:

@interface Foo : Superclass <Protocol1, Protocol2>
// ...
@end

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:

@interface Foo : Superclass <Protocol1, Protocol2>
{
    Bar *bar;
}

@property(nonatomic, retain) id <DelegateProtocol> delegate;

- (void)methodWithArg:(Baz *)baz;
- (Qux *)qux;

@end

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:

@class Bar;
@class Baz;
@class Qux;
@protocol DelegateProtocol;

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:

#import "Superclass.h"
#import "Protocol1.h"
#import "Protocol2.h"

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.

Code smell: Too many #imports in .m

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:

  • You add certain #imports by habit when starting a new class because they’re part of your regular toolkit. But you never actually use every tool.
  • You remove an object reference from your class. But you never go back and remove its header.

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.

Summary

Let’s bring it all home! Here are the things to look for to manage file dependencies:

#imports in header files:

  • #import the superclass you’re inheriting, and the protocols you’re implementing.
  • Forward-declare everything else (unless it comes from a framework with a master header).
  • Try to eliminate all other #imports.
  • Declare protocols in their own headers to reduce dependencies.
  • Too many forward declarations? You have a Large Class.

#imports in implementation files:

  • Eliminate cruft #imports that aren’t used.
  • If a method delegates to another object and returns what it gets back, try to forward-declare that object instead of #importing it.
  • If including a module forces you to include level after level of successive dependencies, you may have a set of classes that wants to become a library. Build it as a separate library with a master header, so everything can be brought in as a single prebuilt chunk.
  • Too many #imports? You have a Large Class.

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! Please share your discoveries or questions in the comments below.

[This post is part of the Code Smells in Objective-C series.]

Photo by Thomas Hawk (license), adapted by Jon Reid

Jon Reid

Posts Twitter Facebook Google+

I'm passionate about not just improving our code, but improving the way we code.

15 responses to #imports Gone Wild! How to Tame File Dependencies

  1. Thanks you guy, very good article.
    I have one question, is any tool available to finger out header file dependencies?

    • Jianhua, that’s a great question! Unfortunately there doesn’t seem to be a good solution at present for Xcode developers.
      A heavy-reading book on the topic is Large-Scale C++ Software Design by John Lakos. He offers a detailed analysis, including mathematical models for measuring when dependencies become too much.
      cppdep is a project based on the book. I tried altering it today to look at .m files, but couldn’t get it to work.
      Ideally, an Xcode-specific tool will examine the project settings. Hmm, you have me thinking about working on this…

  2. The ‘include what you use’ tool looks promising, but it doesn’t support Objective-C. include what you use
    However, there as branch for Objective-C support, but I don’t know how well it works: objective-c support

  3. Very nice post, Jon. This confirms what I usually do with my design. And that’s good news. Will go read up Large Class in Refactoring that I bought last year. But before I do, I am thinking “factory method”..

    - Herman

    • Herman, Large Class is one I see often. People rarely set out to write a Large Class. It happens gradually. There’s a lot to learn from the list of code smells in the Refactoring book!

  4. That’s why I love the IDE Appcode so much. It has great features like color coding the #imports which are unnecessary and an optimization tool for imports.

    • Nick,
      Clearly, I need to learn more about AppCode and its features!

    • AppCode is very nice, but I’ve found that sometimes it removes some imports that actually are required.

      • A work in progress, I suppose. Is AppCode worth the pain of having to switch to Xcode occasionally (for editing nibs, I suppose)?

        • AppCode adds a new level of productivity to objective-c development, so I clearly can say yes. It’s not supposed to be a full replacement for XCode but clearly it is for writing code. It doesn’t provide any means to edit the project file nor interface builder files. The same project can be open in both applications without any trouble.

  5. Thanks, great article!
    I have a question though. You mentioned the importance of the order of #imports. Could you elaborate a little about that, how do you sort your #imports ?

  6. Do I should add some #imports, if I must bring in some non-object declarations? Or other way?

  7. Hm, missed your great article and wrote another one about the same topic, but with a different example: Forward Declaration in Objective-C

Leave a Reply

*

Text formatting is available via select HTML.

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> 

Have you Subscribed yet? Don't miss a post!