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.