7.1.1.  Organizing Libraries: Dependency Management

[ fromfile: liborg.xml id: liborg ]

A dependency between two program elements exists if one reuses the other; that is, if building, using or testing one (the reuser) requires the presence and correctness of the other one (the reused). In the case of classes, a dependency exists if the implementation of the reuser class must change whenever the interface of the reused class changes.

Another way of describing this relationship is to say that ProgElement1 depends on ProgElement2 if ProgElement2 is needed to build ProgElement1.

This dependency is a compile time dependency if ProgElement1.h must be #included in ProgElement2.cpp to compile.

It is a link time dependency if the object file ProgElement2.o contains symbols defined in ProgElement1.o.

Figure 7.1 shows the dependency between a reuser ClassA and a reused ClassB with a UML diagram.

Figure 7.1.  Dependency

Dependency

A dependency between ClassA and ClassB can arise in a variety of ways. In each of the following situations, a change in the interface of ClassB might necessitate changes in the implementation of ClassA.

In each case, it is necessary to #include ClassB in the implementation file for ClassA.

In the package diagram shown in Figure 7.2, we display parts of our own libs collection of libraries. There are direct and indirect dependencies shown. This section focuses on the dependencies between libraries (indicated by dashed arrows).

Figure 7.2.  Libraries and their Dependencies

Libraries and their Dependencies

If you want to reuse one of the libraries shown in Figure 7.2, you need to ensure that all of its dependent libraries are also part of your project. For example, if you use the filetagger library, there is a chain of dependencies that requires you to also make available the dataobjects library (e.g., MetaData classes are derived from DataObject), and the taglib library (e.g., filetagger uses taglib to load metadata). If you want to use sqlmetadata, then you need QtSql, the SQL module of Qt.

Code reuse, a valuable and important goal, always produces dependencies. When designing classes and libraries, you need to make sure that you produce as few unnecessary or unintentional dependencies as possible because they tend to slow down compile times and reduce the reusability of your classes and libraries. Each #include directive produces a dependency and should be carefully examined to make sure that it is really necessary. This is especially true in header files: Each time a header file is #included it brings all of its own #includes along with it so that the number of dependencies grows accordingly.

[Note] Note

A forward declaration of a class declares its name as a valid class name but leaves out its definition. This permits that name to be used as a type for pointers and references that are not dereferenced before the definition is encountered. Forward declarations make it possible for classes to have circular relationships without having circular dependencies between header files (which the compiler does not permit).

In a class definition header file, one good rule to follow is this: Do not use an #include if a forward declaration suffices. For example, the header file "classa.h" might look something like this:

  #include "classb.h"
  #include "classd.h"
  // other #include directives as needed
  class ClassC;    // forward declaration
  class ClassA : public ClassB {
    public:
      ClassC* f1(ClassD);
    // other stuff that does not involve ClassC
  };

There are (at least) two intentional reuse dependencies in this definition: ClassB and ClassD, so both #include directives are necessary. A forward declaration of ClassC is sufficient, however, because the class definition only uses a pointer to that class.

Dependency management is an important issue that is the subject of several articles and for which a variety of tools have been developed. Two open source tools are



[53] We discuss signals and slots in Section 8.5.