In this guide, we are going to explore circular dependency, a scenario where two or more entities (modules/classes/components) have a direct or indirect mutual dependency. Essentially, circular dependency occurs when the execution or compilation of a module or component necessitates another module or component that relies on the initial module or component as a requirement.
Understanding Circular Dependencies:
Cyclic dependencies occur when three or more classes rely on each other, whether through direct or indirect relationships. For instance, in this scenario, Class A and Class B have mutual dependencies, with Class B relying on Class A, resulting in a circular dependency structure.
Example:
// Class A uses Class B
class A {
B b; // the class A uses the class B
};
// The class B uses a class A
class B {
A a; // The class B uses A
};
In class A, it contains a member of class B type, and class B contains a member of class A type. Nevertheless, this scenario will pose challenges for the compiler as it struggles to determine the order in which classes should be constructed due to circular dependencies.
Resolving Circular Dependencies:
Methods like dependency injection and inversion of control are implemented to minimize circular dependencies in the codebase.
1. Forward Declarations
Declaring classes before their definitions is a common practice known as forward declarations. This technique enables the compiler to understand the structure of a class without requiring the full details of its implementation. The subsequent example demonstrates the use of forward declaration in C++:
Example:
class B; // the forward class declaration
class A {
B* b; // The class A uses the class B
};
class B {
A* a; //The class B uses the class A
};
In this provided code snippet, there exists a circular dependency between class A and class B. Both classes store a reference to the other class, creating a dependency loop. To address this issue, forward class declarations are used to break the circular dependency.
Forward Class Declaration:
- This declaration affirms the existence of class A without providing the complete definition of class B.
- It allows for declaring pointers to class B within class A without needing the header file of class A.
Class A:
- This consists of a reference pointer b pointing to an instance of class B.
- Encompasses the preliminary declaration of class B, allowing compilation without the complete definition of class B.
Class B:
Teaching Icpp concepts to a member of class A.
This involves utilizing the forward declaration of class type A, enabling the inclusion of pointer objects to class A within its definition.
2. Minimize Header Inclusions:
By refraining from header inclusion, we ensure that only essential headers are included in each source file. This approach reduces the reliance on variables, resulting in a quicker process by loading only the vital declarations.
Explanation:
- In C++, each source file normally incorporates header files of the ones it uses.
- Obtaining alternative header files might increase the compilation time and necessitate coupling between the classes.
- In this way, you decreased the number of headers that have to be included and reduced the possibility of circular dependencies between the dependencies.
Example:
#include "Class1.h"
#include "Class2.h"
//Only necessary headers are included.
3. Pointer or Reference Usage:
Instead of providing full class definitions directly in the code, opt for pointers or references to those classes whenever feasible. This approach promotes class independence, allowing intermediate classes to be forward-declared.
Explanation:
- Where a class needs to communicate with another class, it can employ pointers or references to objects of the respective class rather than including its header file.
- Pointers or references are used within forward declarations of classes because the compiler is unable to recognize the class definition but is aware of the class's sheer existence.
Example:
// ClassA.h
class ClassB; // The forward class declaration
class Class {
ClassB* aPtr; //Pointer referencing to the class B
};
This approach facilitates separation between classes, aiding in the organization of code.
4. Dependency Inversion Principle (DIO)
The Dependency Inversion Principle (DIP) is a design concept that emphasizes the structure of relationships between different modules or components within a software system. It suggests that higher-level modules should not have direct interactions with lower-level modules, but instead, both levels should depend on shared abstractions.
5. Separate Interface and Implementation:
Avoiding the inclusion of the interface in the implementation (public methods and member functions) diminishes the interconnection among classes. This allows classes to substitute the implementation with the interface, resolving any circular dependencies they may encounter.
Explanation:
- With the use of interfaces or abstract classes, we can define a contract that demonstrates expected behavior without revealing implementation details.
- Classes can utilize the interface and may switch between implementations, thus reducing coupling between classes.
- It helps in easy maintenance and testing of code.