Search code examples
design-patternsdependenciesdependency-managementcircular-dependency

Circular dependencies between classes: why they are bad and how to get rid of them?


I feel circular dependencies (aka circular references, or cycles) mean bad design and harm the project. How can I convince my teammates and my manager?

My project is a dependency mess. Is there a methodology to get rid of wrong dependencies and then maintain the clearness?

It is puzzling how to organize folders and libraries to make it simple for developers to decide where to put new code (or how to refactor old one), so that (1) there is no cycles, (2) it is easy to navigate. How such a structure may look like?


Solution

  • What can be easy to maintain structure?

    The structure I use in my projects is this:

    framework   (or just file 'main' if there is nothing else,
     |           something high level and not allowed to depend on)
     v
    features    (or areas or panes or screens etc,
      feature1   items here are not allowed to depend on each other)
      feature2
      feature3
      ...
     |
     v 
    shared      (code, referenced by number of features)
      shared1
      shared2
      ...
    

    Each item can repeat the structure inside it. For example, if feature1, or shared or shared1 are big and complicated, they may also can contain framework, components and shared.

    Folders with many items may benefit from a subfolder primitives, where each item is independedn in this context. In other words, each item in my_folder/primitives do not depend on other items in my_folder.

    See example.

    You can use tools like LayerLens to auto-detect issues on pre-submit.

    Why are circular dependencies (cycles) bad?

    Two reasons:

    1. Maintainability.

    You want your code to be layered, i.e. you want to have a top-down diagram of dependencies (a diagram showing all arrows going down, and no arrows going up). If you have cycles, your code is not layered.

    Why does the layered code mean maintainability? Because, every time you change the interface of a class, you can be sure that nothing below it will be affected. This knowledge makes maintenance and development of the system cheaper and less error-prone.

    dependency diagram example

    Layerlens can auto-generate dependency diagrams for your project.

    1. Reliability.

    You do not want unexpected infinite recursion in production. For example, when your configuration wants to log an error and your error-logger wants to read the name of the log file from configuration (your tests will pass, because there is no error in the test environment). (Unfortunately, unexpected recursion can be developed even without cycles.)

    Are there good cycles?

    Some cycles are valid, helpful, and do not affect maintainability or reliability: String and Object, File and Folder, Node and Edge. Usually, such circles sit within one package and do not contribute into circular dependencies between packages.

    How do I detect cycles in a package?

    You can detect cycles using tools like LayerLens.

    If your project is huge or you want to watch for cycles continuously, it is simple to implement a tool that uses reflection to detect cycles (traverse classes or packages depth-first and stop at the first back reference).

    Note, that if one package is declared inside other, this does not mean they depend on each other, unless classes from them reference each other.

    My project is a dependency mess. Is there a methodology to fix it?

    dependency mess Yes, there is. And here is the steps:

    1. Design the desired structure. See the suggested structure above.

    2. Make the process measurable.

    Measure the distance to the desired structure so that you can see the progress as you get closer to the goal. For each class, count the number of wrong dependencies (dependencies directed up). The sum of these numbers will become your metric. At the end, it will be zero.
    Set up monitoring to detect new wrong dependencies on presubmit.

    1. Resolve wrong dependencies one by one. Usually it is easier to go from bottom to top, as you’ll want to clarify the basics first.

    These tips may help:

    A. You have a "death star” or “god object,” i.e. an object that is known by classes it knows about. In other words, such a class is a part of many circular dependencies which may result in the dependency of each class on almost every other class.

    Solution:
    Most death stars can be resolved by splitting them into two (or more) classes, where one class contains just state and very basic operations and another class contains advanced operations (usually the second class is static). enter image description here

    B. You do not have a circle between classes, just between packages.

    Solution:
    Consider moving some classes to other packages.

    C. You have a class that uses some methods of upper class, but does not own its instantiation.

    Solution:
    Use a callback interface or callback method (pattern Observer). enter image description here

    D. You have two classes that create each other and use each other’s methods.

    Solutions:

    • Combine the classes into one class.
    • Put the classes into one package and declare their relationship as a good cycle.
    • Create a Factory class and interface for one of the classes.

    enter image description here

    How can I maintain clarity in my project?

    If you have a small team and a small project, simply explain the rules to your teammates and occasionally check the diagram.

    If the project is large and complicated, you should establish a process to receive an alert or rejection every time someone compiles or checks in a wrong dependency.