Organizing Libraries: Dependency Management

Organizing Libraries Dependency Management

A dependency between two program elements exists if one is reusing the other, that is, if 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 be changed whenever the interface of the reused class is changed.

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

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

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

We depict the dependency between a reuser ClassA and a reused ClassB with a UML diagram, as shown in Figure 7.1.

Figure 7.1. 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 have displayed parts of our own libs collection of libraries. There are direct and indirect dependencies shown. At this level of granularity we are concerned with the dependencies between libraries (indicated by dashed arrows).

Figure 7.2. Libraries and their dependencies

If you wish 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., FileTagger is derived from DataObject), the utils library (e.g., the test code generally reuses the various assert macros in utils), and the id3lib library (e.g., FileTagger has a member of type auto_ptr and defines some functions with parameters of type ID3_FrameID*).

Code reuse, a valuable and important goal, always produces dependencies. When designing classes and libraries it is important to make sure that we 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.

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 will not permit).

In a class definition, one good rule to follow is this: Do not use an #include if a forward declaration will suffice. 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 };

We have (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, since the class definition only involves a pointer to that class.

It is very important to make sure that there are no circular dependencies in your project (i.e., in a diagram like the one in Figure 7.2, there must be no path that permits you to return to the starting location by following a sequence of arrows.)[3]

[3] Such a path is called a cycle.

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

7.3.1. Installing Libraries

After a library has been written and tested, it will be installed at the end of the build process in the directory specified by the DESTDIR variable. For example, the project file for our utils library contains the following relevant lines:

TEMPLATE = lib # Build this as a library, not as an application DESTDIR=$$(CPPLIBS) # Place the compiled shared object code here

For library templates, qmake can generate a Makefile with the install target so that the command

make install

will, after a successful build, copy the library to some particular location. For example, on a *nix platform, the following lines could be added to the project file for utils:

target.path=/usr/lib INSTALLS += target

Then, if you have write access there, the command

make install

would copy the libutils.so files and their associated symlinks to the directory /usr/lib.

If you need to relocate a library, the procedure varies from platform to platform. In Windows, you can copy its .dll file into an appropriate directory that is listed in your PATH variable. In *nix, you can copy the shared object file and associated symbolic links into a directory that is listed in /etc/ld.so.conf or one that is findable by searching in LD_LIBRARY_PATH.

During development, it is usually sufficient to make and install libraries in your home directory, and adjust LD_LIBRARY_PATH appropriately. At deployment time, on a *nix platform, it may be desirable to install the library in /usr/local, a systemwide location accessible to all other users. This would require superuser permissions.

Installing Libraries A Lab Exercise

Категории