Cyclic Dependencies

Using Escalation to Solve Cyclic Dependencies

As we discussed in the previous blog post, cyclic dependencies degrade the quality of a software system, leaving it inflexible and difficult to manage. As the size of a software system grows, the importance of catching and fixing cyclic dependencies grows because the overall cost of developing and maintaining the system increases.

How Cyclic Dependencies are Created

Software systems usually start out with clean, well thought-out designs that don’t have bad dependencies (cyclic dependencies). But, as software development starts in earnest, custom enhancements or bug fixes are requested, usually with quick turnaround times. This can lead to situations where the design is compromised because of insufficient development time and can lead to architectural problems like cyclic dependencies.

As an example, here is some C++ code that has two similar classes in a software system that do similar things (hold similar info), Circle and Tire, both representing a circular shape. A Circle is a defined by a radius.

// circle.h
class Circle {
// ...
public:
    Circle(int Radius);
    // ...
    double getarea();
};

While a Tire is defined by a center point and a radius.

// tire.h
class Tire {
// ...
public:
    Tire(int xCenter,
           int yCenter,
           int radius);
     // ...
     double getCircumference() const;
};

Both provide similar functions and hold similar information but might also have custom functionality or different performance characteristics. What if we now have a client request to convert between these two types of circular shapes? One solution is to add a constructor which has one argument: a const reference to the other class. In this solution, the conversion is performed implicitly. Allowing two components to “know” about each other via #include directories implies a cyclic dependency.

// circle.h
#include "tire.h"

class Circle {
// ...
public:
    Circle(int Radius);
    Circle(const Time &T);
    // ....
    double getarea();
};
// tire.h
#include "circle.h"

class Tire {
// ...
public:
    Tire(int xCenter,
           int yCenter,
           int radius);
     Tire(const Circle &c);
     // ...
     double getCircumference();
};
This solution has two problems:
  • Performance degrades because you have to create a temporary object of the other type when you call the constructor.
  • You have introduced a cyclic dependency between two components. Now you can't compile, link, test, or use one class without the other.

How to Solve Cyclic Dependencies with Escalation

One solution is to move the functionality that is causing the cyclic dependency to a higher level. If peer components are cyclically dependent, it may be possible to escalate the interdependent functionality from each of these components to static members in a potentially new, higher level component that depends on each of the original components. We can create a utility class calledCircUtil that knows about the Circle and Tire classes and place the definitions in a separate component.
// circutil.h

class Circle;
class Tire;

struct CircUtil {
    static Tire toTire(const Circle &c);
    static Circle toCircle(const Tire &t);
};
// circle.h

class Circle {
    // ...
    public:
    // ...
};
// tire.h

class Tire {
    // ...
    public:
    // ...
};
Now you can use, test, etc. each class independently.

Summary

While this is a simple example, cyclic dependencies in larger systems have the potential to greatly increase the cost of developing and maintaining software systems. Escalating mutual dependencies to a higher level can fix cyclic dependencies. This reduces the maintenance costs of a system by getting rid of unnecessary dependencies among components at the same level. This also makes the system more flexible and reusable. If you are interested in identifying your bad dependencies, check out Lattix Architect.

Why Cyclic Dependencies are Bad

In software development, divide and conquer is a design strategy where you recursively break down a problem into two or more sub-problems, until the problem becomes simple enough to be solved directly. This is where software components (packages, assemblies, modules, classes, etc.) come into play. Components break up large blocks of code into smaller, more manageable pieces. One rule of thumb is a component should only include closely related code. This makes deployment and maintenance easier to manage. Ideally, you would like all components to be independent of each other, but inevitably some dependencies are necessary.

Levelization

Often we quantify code as “high-level” or “low-level”. This is a standard way of managing dependencies (levels). You have high-level layers and low-level layers. Each layer should depend only on the layers below it, not on any layer above it. If you are new to a codebase, it is often helpful to understand what the high-level code is and what the low-level code is.

This is helpful because you can make sure that low-level code does not inadvertently rely on high-level code. You can easily order your components using Lattix Architect by applying component partitioning to the DSM. This levelization is important because that is how you divide and conquer. First you divide the software into components, then you conquer by making sure there are no dependencies between components. In a typical embedded software system, there is usually a high-level communications layer, a middle hardware abstraction layer (HAL), and a low-level drivers layer. Here is a standard picture (standard layer):

Standard Software Architecture

Having a dependency from the bottom layer to the top layer is a circular dependency (cyclic dependency).

Cyclic Dependency

Because of the cyclic dependency, there is no layering between components. They are all on the same layer (one giant component).

Giant Software Component

This has ruined the “divide and conquer” approach of having components. Instead of having three components, now you have one giant component that is three times larger and much more complicated and can not be developed or tested independently. (With Lattix Architect, it is easy to see cyclic dependencies in the DSM after it has been partitioned).

Why Cyclic Dependencies are Bad

Cyclic dependencies between components inhibit understanding, testing, and reuse (you need to understand both components to use either). This makes the system less maintainable because understanding the code is harder. Lack of understanding makes changes harder and more error-prone. Also, if components are in a circular dependency they are more difficult to test because they can not be tested separately. Cyclic dependencies can cause unwanted side effects in a software system. When you make a small change to a software system it can cause a ripple effect to other modules, which can have global ramifications (bugs, crashes, etc.). Finally, if two modules are tightly coupled and mutually dependent on each other, reuse of an individual module becomes extremely difficult or even impossible.

Summary

Cyclic dependencies are bad. If you find that components are in a cycle with each other, there are three things you can do:

  1. Repackage them so they are no longer mutually dependent
  2. Combine them into a single component
  3. Think of them as if there were a single component

The best solution is to detect and correct cyclic dependencies as soon as they occur. You can do this by checking your architecture regularly. A tool like Lattix Architect can help.