When I attempted to write my first object oriented program with some meaningful complexity I was hit by the problem of cyclic/circular dependency. As the name suggests the problem definition is very simple. If we need to design two classes which need to call functions of each other directly how do we accomplish that? And what are the issues we may land up into if we do not pay proper attention to avoid circular dependency among them. I am sure this problem would have come up with many other programmers like me. In this post I would like to get in depth of this problem and explain why its call an anti-pattern and what are possible approaches to avoid it.
An anti-pattern is a complement of a design pattern. As per wiki it is defined as “An anti-pattern (or Antipattern) is a pattern used in social or business operations or software engineering that may be commonly used but is ineffective and/or counterproductive in practice”. I will stick to software engineering domain here. As programmers we may be using one or many of the design patterns in implementation of our solutions, similarly there may be several anti-pattern in our code as well which may escape our attention and potentially cause issues later.
Circular dependencies can cause many unwanted
effects in software programs such as.
- Tight coupling of the mutually dependent modules which reduces or makes impossible the separate re-use of a single module.
- Domino effect when a small local change in one module spreads into other modules and has unwanted global effects (program errors, compile errors).
- Infinite recursions or other unexpected failures.
- Memory leaks by preventing certain very primitive automatic garbage collectors (those that use reference counting) from deallocating unused objects.
- Un-wanted compilation error due to inclusion of header files in one another.
Circular dependency can occur in many scenarios the most common is the containment relationship between two classes with the child class needing to call some functions in the creator class.
For example let’s say that we have a School, Student and Teacher classes. Ideally school will contain the student class and teacher class and will call functions on it. Similarly teacher will hold handles of all the students it is directly teaching. But what if a student class needs to call a function of the teacher to give feedback. Similarly how shall we model a call from a teacher to school let’s say if teacher wants to resign. This is a classic case of circular dependency between classes School, Teacher and Students. Here is the static class structure diagram of the given problem.
In the header class of school, we include header of student of and teacher classes. Similarly in the header of teacher class we will include header of student class. But if we try to include header of teacher class inside student class header then we are in for a trouble. We will get compilation issue if we do not order the headers properly. We need to keep the header inclusion acyclic. But then how do we get the definition of teacher class in student class without including its header. Our first rudimentary solution to fix the inclusion issue is forward declaration let’s see how it’s done
1) Forward Declaration Solution
In this solution we do not include header files for school and teacher class in student class header file. Rather we forward declare teacher class and school class before defining the student class and only in the .cpp file of student class we include headers of teacher class thus avoiding the circular header dependencies.
The complete vc++ workspace based on forward declaration is available in the following link(circularDependency). Look for forward declaration for school_t and teacher_t class in header file of student class. Also the header file of school and teacher class are included only in the .cpp file of student class.
Drawbacks of this approach: This approach works well in simple cases but it become complicated to inline any function that uses the forward declared class variable.
Secondly this approach fails if we have any templatized variable/functions in the class because in case of templatized variables, the implementation of function using template variable has to be done in the header file itself.
Drawback of this approach: This approach also has the same drawbacks if the number of functions are large in numbers. Moreover in both these approach a very tight coupling between the class is maintained.
The workspace depicting c/c++ function pointer approach is available at this link(circularDependencyV2). The circular dependency between student and teacher classes(look for give_feedback function) is broken using C function ptr approach. Similarly the dependency between teacher and school class(look for resign function) is broken using C++ function ptr approach. The complete vc++ workspace based on forward declaration is available in the following link(circularDependency). Look for forward declaration for school_t and teacher_t class in header file of student class. Also the header file of school and teacher class are included only in the .cpp file of student class.
Drawbacks of this approach: This approach works well in simple cases but it become complicated to inline any function that uses the forward declared class variable.
Secondly this approach fails if we have any templatized variable/functions in the class because in case of templatized variables, the implementation of function using template variable has to be done in the header file itself.
2) C Function pointer solution
In this solution we do not need to include header of teacher class even in the source file of student, rather for all the calls required from student to teacher class we define a function pointer and that function pointer is member of student class that gets filled in at the time of construction. The C function takes a void * ctx and then using the ctx the instance of teacher class is resolved.
Drawback of this approach. This approach works well if the function from the student to teacher class are less in number but if they are more in number then coding all of them become error prone. Similarly the chances of crashes are there if this function ptrs are not populated and are called. Aditionally a ctx has to be managed and there is one level indirection from a C function to a C++ function.
3) C++ Class Function pointer solution
This solution is similar to the C function pointer solution but it removes the need for maintaining the ctx and also an indirection from C to C++ function is not required.Drawback of this approach: This approach also has the same drawbacks if the number of functions are large in numbers. Moreover in both these approach a very tight coupling between the class is maintained.
4) Boost Function solution
Boost function is a very generic function definition that can get binded to any C function or C++ function. So using this approach we can declare boost function with a given function signature(only parameter and return types).
And at the time of calling the function any function can be binded to the boost function adhearing to the function signature. Both C, C++ and boost approach are similar in nature. School class contains both student and teacher class. Teacher class include the header of student class. Student uses a boost C function to talk to teacher class. Teacher uses a boost C++ function to talk to school. The point to note here is that student class does not include header of school class or teacher class in .h or .cpp file. This way the cyclic dependencies between student, teacher and school classes is broken.
The workspace using this approach is available at this link(circularDependencyV3).
Drawback of this approach: Not part of standard C++, Complicated syntax, Performance not that great.
5) Interface Solution
The most efficient and elegant way to break cyclic dependencies among class is use of interfaces for all the classes as per their relationship. For the given example student class implements functionality to be called by teacher and school directly. Similarly teacher implements functionality to be called by school directly. The functions to be called by school can be simply exposed from teacher class but the functions to be called by student can be put in an interface class or abstract base class called student_to_teacher_intf_t and teacher derive from it.
Similarly the functions to be called by teacher into school class can be defined in in interface class called teacher_to_school_intf_t.
The updated class diagram for implementation is.
The workspace implemented using interface solution is available at this link(circularDependencyV4).
Conclusion:
I have tried to list down all the most commonly used methods for breaking cyclic dependencies among the classes. No solution is suited for each and every scenario, hence choose the solution as per your need. However using the interface based solution is most elegant and efficient and it is my preferred solution.
No comments:
Post a Comment