Discussion
Team LiB
Previous Section Next Section

Discussion

Prefer forward declarations except where you really need a type's definition. You need a full definition of a class Cin two main cases:

  • When you need to know the size of a C object: For example, when allocating a C on the stack or as a directly-held member of another type.

  • When you need to name or call a member of C: For example, when calling a member function.

In keeping with this book's charter, we'll set aside from the start those cyclic dependencies that cause compile-time errors; you've already fixed them by following good advice present in the literature and Item 1. Let's focus on cyclic dependencies that remain in compilable code, see how they trouble your code's quality, and what steps need be taken to avoid them.

In general, dependencies and their cycles should be thought of at module level. A module is a cohesive collection of classes and functions released together (see Item 5 and page 103). In its simplest form, a cyclic dependency has two classes that directly depend upon each other:



class Child;                // breaks the dependency cycle





class Parent {// …


  Child* myChild_;


};





class Child {// …            // possibly in a different header


  Parent* myParent_;


};



Parent and Child depend upon each other. The code compiles, but we've set the stage for a fundamental problem: The two classes are not independent anymore, but have become interdependent. That is not necessarily bad, but it should only occur when both are part of the same module (developed by the same person or team and tested and released as a whole).

In contrast, consider: What if Child did not need to store a back link to its Parent object? Then Child could be released as its own separate, smaller module (and maybe under a different name) in total independence from Parentclearly a more flexible design.

Things get only worse when dependency cycles span multiple modules, which are all stuck together with dependency glue to form a single monolithic unit of release. That's why cycles are the fiercest enemy of modularity.

To break cycles, apply the Dependency Inversion Principle documented in [Martin96a] and [Martin00] (see also Item 36): Don't make high-level modules depend on low-level modules; instead, make both depend on abstractions. If you can define independent abstract classes for either Parent or Child, you've broken the cycle. Otherwise, you must commit to making them parts of the same module.

A particular form of dependency that certain designs suffer from is transitive dependency on derived classes, which occurs when a base class depends on all of its descendants, direct and indirect. Some implementations of the Visitor design pattern leads to this kind of dependency. Such a dependency is acceptable only for exceptionally stable hierarchies. Otherwise, you may want to change your design; for example, use the Acyclic Visitor pattern [Martin98].

One symptom of excessive interdependencies is incremental builds that have to build large parts of the project in response to local changes. (See Item 2.)

    Team LiB
    Previous Section Next Section