Separation of implementations from interfaces can be a good idea and can reduce the complexity of the dependency graph of a build tree since users of a capability need to depend only on the interfaces and not the implementations. If done incorrectly, however, separating implementations from interfaces has several pitfalls. One may be tempted to implement separation of interfaces from implementations by using a scheme such as the one described in the previous section, Section 30.5, “Hidden Dependencies”. In addition to creating a potential hidden dependency issue, it is possible to create even worse situations, such as hidden circular dependencies.
The case in the previous situation showed how we can create a
link error that could be resolved by adding an extra dependency
in main
. It is relatively easy to create
situations that will cause unresolvable link errors as well by
creating separate header-only build items. For example, suppose
you have libraries A
and
B
and separate build items
A-headers
and
B-headers
to export their static header
files. Suppose now that A
depends on
A-headers
and
B-headers
and that
B
also depends on
A-headers
and
B-headers
. (See Figure 30.1, “Hidden Circular Dependency”) In this case,
A
and B
are
actually interdependent but there are no circular dependencies
declared. If there are any situations between
A
and B
in which
the first reference to something in B
appears in A
and the first reference to
something else in A
appears in
B
, then anything that depends on
A
and B
will have a
link error.
[62]
This is a hidden circular dependency. The best way to avoid this
situation is to not split A-headers
from
A
.
Figure 30.1. Hidden Circular Dependency
A
and B
are
interdependent even though no explicit circular dependencies
exist.
There are other less insidious problems that are still annoying.
For example, A-headers
might really depend
on B-headers
but forget to declare this.
As long as A-src
declares a dependency on
B-headers
, we'll never notice that
A-headers
forgot to declare its dependency
because A-headers
isn't actually built.
We might later try to build something else that declares a
dependency on A-headers
. This other build
may fail because of B-headers
not being
known. We've then created a hidden dependency situation: anyone
who depends on A-headers
must also depend
on B-headers
. The best way to this
situation is also to not split A-headers
from A
.
One cost of not separating these is that if one library depends only on another library's header files, the two libraries could be built in parallel. By making one library depend on the other in its entirety, abuild will force the other library to be built before the dependent library. This is unfortunate, but it's not a good idea to work around this by introducing holes in abuild's dependency management. A better technique would be to use some external analyzer that could detect at a finer level what things can actually be built in parallel. There are commercial tools that are designed to do this. Perhaps, over time, abuild will acquire this capability, or users of abuild can implement some solution on top of abuild that uses an external tool.
Proper separation of interfaces from implementations, such as using a bridge pattern (as described in the Design Patterns book by Gamma, et al), which allows the implementation and interface to vary separately by implementing proxy methods that call real methods through a runtime-supplied implementation pointer, can solve the parallel build problem without introducing any of these other pitfalls. Ultimately, as long as you don't create a situation where depending on one thing automatically requires you to depend on some other specific thing to avoid link errors, you should be in pretty good shape. You can also see an example of true separation of interfaces from implementations in Section 25.2, “Mixed Classification Example”.
Note that another way to create this hidden dependency problem is to create a directory that contains header files for multiple build items. Suppose, for example, that you have the directory structure shown in Figure 30.2, “Shared Include Directory”:
and that A
and B
both have their header files in the include
directory. If both A
and
B
add ../include
to
INCLUDES
in their
Abuild.interface
files, any build item that
depends on A
could accidentally include
B
's header files and therefore
accidentally require B
as well. A simple
way to avoid this without having to distribute the public header
files throughout module
's directory
structure would be to create separate directories under include,
such as shown in Figure 30.3, “Separate Include Directories”.
Figure 30.3. Separate Include Directories
Headers are still easy to find and are separated by build item.
[62] Use of shared libraries or repeating libraries in the link statement could actually work around this specific case, but there are good reasons to avoid circular dependencies beyond just making abuild happy. The point is that this technique allows them to hide in the dependency graph, which is a bad thing.