30.6. Interfaces and Implementations

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

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”:

Figure 30.2. Shared Include Directory

Shared Include Directory

Oops! Both build items use the same 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

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.