Handling Circular Dependencies in yotta

It is possible for two or more yotta modules depend on each other in a loop. In general this should be avoided. It might indicate that your modules are strongly coupled, and perhaps should be shipped as a single module or that they should be divided up differently. If however it is necessary to use circular dependencies, that’s something that yotta supports. In this case there are some guidelines that can help you handle circular dependencies successfully.

# Alternatives to Circular Dependency Patterns

If you can separate your modules in ways that avoid circular dependencies, then this is preferred. Some common circular dependency patterns have simple alternatives:

# Interdependent Modules

illustration of interdependent modules

If two modules (a and b above) both call functions from each other, this can be an indication of two things:

Either these modules are on the whole not independent, and should be combined into a single module:

illustration of combined module

Or these modules are largely independent, but a small parts of both of them are strongly connected. In this case at least one of these parts should be separated into a third module, on which each module then depends. This only needs to remove one of the dependencies between the two modules to break the cycle:

illustration of separated module

# Abstract API with Multiple Implementations

Sometimes you have an API that has multiple possible implementations (which might be for different hardware platforms, or different network transports, for example).

In this case you might have a module, foo, which provides some re-usable functionality. foo itself might depend on foo-implementation-windows, or foo-implementation-posix depending on what platform it’s being built for, if the functionality it is providing is implemented in different ways on different platforms.

Each of these implementations would in turn depend on the foo module in order to have access to the header files that declare the interface they are implementing, leading to a circular dependency. For example:

illustration of circular dependencies

This circular dependency can be avoided by moving the header files with declarations into a third module, called foo-api below, on which both of the other modules depend. In this case foo is still the public module that other people would depend on (and it could contain header files that include appropriate foo-api headers, so that users do not need to know anything about thefoo-api module’s existence).

illustration of resolving circular dependencies

# Handling Circular Dependencies Successfully

# Version Specifications

If you have a set of modules which depend on each other in a cycle, and which all impose version specifications around that cycle, then this can make it very hard to update to new versions of any of the modules as updating any one module at a time might break the version requirements of others.

To prevent this situation from arising, one point in the cycle needs to have a relaxed version specification (such as the wildcard “*”).

illustration of circular dependency version specifications

A simple rule to do this is to identify the modules in the cycle which will only normally be depended on by other modules that are also in the cycle (such as API implementation modules, as described above). Since these modules won’t normally be depended on externally, they can have wildcard “*” version specifications on the other modules in the cycle on which they depend. The versions of these dependencies will instead by controlled by the external dependencies on them.