Friday, March 14, 2014

What to do with Dependency Injection Cycles

If you've used dependency injection, at some point you've probably run across a dependency graph cycle, also known as a circular dependency. This post looks at why cycles can be problematic and ways of dealing with them.

Basic Theory

When configuring your Dependency Injection container, you're setting up a directed graph (read if you're not familiar) where the vertices are classes and the edges are dependencies between classes. A cycle in the graph means that somehow there's a mutual dependency among two or more classes. In this post we'll explain why you always want your dependency graph to be a DAG (Directed Acyclic Graph) and how to deal with situations where cycles arise.

Constructor Injection

Practically speaking, you'll likely encounter a would-be dependency graph cycle as a side effect of how most dependency injection frameworks work by default. Usually when a class needs a particular interface it simply makes that interface a constructor parameter, but when there's a dependency graph cycle this no longer works. For example:


interface IA {
    // Members
}

interface IB {
    // Members
}

class A implements IA {
    private final IB mB;

    public A(IB b) {
        mB = b;
    }

    // Members
}

class B implements IB {
    private final IA mA;

    public B(IA a) {
        mA = a;
    }

    // Members
}

Class A needs the IB implementation, and class B needs the IA implementation, but neither can be instantiated because they depend on one another. As you can see, constructor injection does not directly allow dependency graph cycles.

There are ways such as setter injection to create working cycles in your dependency graph, but that isn't really a solution to the underlying design problem. Cycles point to several potential problems in your code, so consider constructor injection's inability to directly support cycles a helpful design tool rather than a bug.

Issues with Cycles

To understand why we don't want cycles in the class dependency graph, we need to look at the method dependency graph. We'll look at examples of the two possible scenarios of graph cycles, whey they're an issue, and how to fix them. Note that these diagrams show both the class dependency graph and the method dependency graph.

Acyclic Method Dependency Graph

In this example notice that the method dependency graph itself does not actually have any cycles, even though the class dependency graph does. In this case, method 2 is really its own responsibility, and class A needs to be broken down further.

Eliminating this class dependency cycle forced us to break the application into smaller pieces, thus improving the application.

Cyclic Method Dependency Graph

This example is rarer and indicates a much bigger problem, because there's actually a cycle in the method dependency graph indicating a likely mutual recursion. The solution can actually be very different depending on whether or not the recursion was intentional.

Solution to Intentional Recursion

If this mutually recursive behavior was intentional, then the solution is to separate the recursive/iterative functionality from the "guts" of the individual methods. Here is one possible solution:

In this example, we've converted the entire recursive aspect of the algorithm into a loop within method 1. The original algorithm probably had four parts because there are really four different pieces of logic to attend to, so in the example we pull those four different parts into new non-recursive methods.

Solution to Unintentional Recursion

If you didn't originally intend to create a mutual recursion then you're likely in for stack overflows and/or major performance problems. In the majority of cases, this is a bug more than a design issue. Unfortunately the actual fix is going to depend completely on your application, though breaking down methods into smaller pieces can often help identify the root cause. Sometimes the cycle in the method dependency graph won't actually create a mutual recursion as a result of the logic, but that situation can later change unexpectedly as a result of a seemingly innocuous modification to the code.

This issue can happen when initialization logic and processing logic are too intertwined. Try moving initialization into a separate class from the one that needs to use the initialized state or configuration. This can help clarify order of operations.

Wrapping Up

Encountering a circular dependency error from your dependency injection container is never fun, but taking the easy road and converting the constructor injection to setter injection is a great way to cause yourself pain later. The majority of the time, circular dependencies happen because some part of the application needed to be broken down into smaller pieces.

I hope these ideas are clear enough to be helpful. Please let me know what you think in the comments.