If this is your first visit, be sure to
check out the FAQ by clicking the
link above. You may have to register or Login
before you can post: click the register link above to proceed. To start viewing messages,
select the forum that you want to visit from the selection below.

Hybrid View

C++ General: How to minimise mutual dependencies?

Q: How to minimise mutual dependencies?

A: A frequently raised problem on CodeGuru concerns the situation where two classes are mutually dependent: A needs to refer to B whilst, at the same time, B needs to refer to A. The simple resolution of this involves forward declarations:

While this works, it does introduce some tight, and unwanted, coupling between the two classes. This means that changes are hard to make because of the explicit dependence between the two classes. Suppose, for example, that the needs of the program change and you now need a version that has to use class C in place of class A - but the original code must still be maintained. In one version, B needs an A* member, but in the other it needs a C*. This sort of thing usually ends up with code littered with conditional compilation sections to cater for the various builds.

It's always a good idea to minimise the coupling between any two entities, so how can we go about improving the mutual dependency situation? The key, as is often the case, is abstraction. We take one of the two dependencies (say B -> A) and abstract it. That is, we look at what B wants to "say" to A, and make an abstract class that defines those functions. we supply this abstract class with the definition of B, and make B dependent on the abstract class:

It's now a simple matter to replace A with C by deriving C from Bcallback as well (you might only need to link two different object files to create the two different versions), since there is no dependency from B to A to complicate matters.

But now, suppose we need to replace B with D: there's still the A/C -> B dependency to cope with. The solution's the same. Abstract out of B the important functions and make an abstract class of them. We keep this abstraction together with the callback interface, and our original B now becomes an implementation of that interface (and is not visible to A). This does introduce the need for some some sort of factory function to create the correct concrete derivative of B, however. This can be either a free function or a static member of B, which is provided along with the implementation of the concrete derivative.

So, in the same way as we shielded A/C from B by hiding it between a layer of abstraction, we have now hidden B(Impl)/D from A/C. A/C can be freely mixed with BImpl/D without the need to introduce conditional compilation sections.

Note: both halves of the abstracted two-way communication are supplied together. Most cases of this sort of dependency result from a client-server relationship between the classes: one class is a client - it uses a service provided by the other. The server class has a need to report progress back to the client at intervals that can't be represented by simple function returns. It's important that the server half of the relationship defines the two abstractions - it's the one providing the service, so it calls the shots.

Note also that we could arrange for BImpl (and D) to accept multiple different instances of classes derived from Bcallback and to send the same message to all of them. And that's called the Observer pattern.