« December 2006 | Main | March 2007 »

January 19, 2007

On Cyclic Dependencies

As the 2007 Spring Experience conference approached, the session I was looking forward to the most was a talk by Spring co-founder Juergen Hoeller entitled "Code Organization Guidelines for Large Code Bases." As the lead of the Spring framework, Juergen deals with a complex growing code base on a daily basis. I was hoping to get some pointers to help guide us through some of the challenges we face dealing with sharing code and resources across multiple projects.

If I was hoping for a prescription for solving our exact issues I would have been disappointed, as the talk did not deal specifically with web applications, but rather with the unique challenges that Spring itself has faced with its code base and how has it solved them. This was not exactly what I was expecting, but I nevertheless came away with quite a bit.

Juergen drove home the importance of one rule above anything else: avoid circular dependencies between modules and packages. The Spring codebase is a testament to Juergen's vigilance in avoiding any circular dependencies. If you examine the code, you will find no circular dependencies of any kind. In fact, even the Spring Petclinic sample application is free of any circular dependency problems. This is in stark contrast to, say, the Hibernate code, which contains many such dependency cycles.

Juergen discussed two tools for dealing with dependency issues, one of which I had heard of and one of which I hadn’t. The one that I had, JDepend, is a free open source tool to analyze dependencies in Java files. It is a command line and code-based tool, though it can be fronted with a GUI. JDepend is great for analyzing code for dependencies and gathering metrics, but it’s not great for figuring out what to do once you’ve identified those dependencies.

That’s where the other tool that Juergen demonstrated, SonarJ, comes in. SonarJ is a newer commercial product that allows you to visualize dependencies coming into and out of a class or package. It also allows you not only to verify that a circular dependency exists, but to trace its path. Overall, our initial evaluation of the product has been very positive.

There is quite a bit of information out there on the theoretical underpinnings and practical application of circular dependency checking. In this publication, the principle is stated as:

THE DEPENDENCY STRUCTURE BETWEEN PACKAGES MUST BE A DIRECTED ACYCLIC GRAPH (DAG). THAT IS, THERE MUST BE NO CYCLES IN THE DEPENDENCY STRUCTURE.

In this paper, Robert Martin attempts to differentiate between good and bad dependencies and discusses some metrics – Afferent Coupling, Efferent Coupling, Instability -- for measuring OO design quality.

However, not everyone is as much of an absolutist about avoiding circular dependencies as you might imagine. Google’s Cédric Beust (of TestNG fame), has argued convincingly that there exist certain cases where such dependencies, at a class level, might be tolerable or even desirable.

Nevertheless, I felt that we needed to take a more aggressive approach to avoiding circular dependencies. So, to that end, I wrote a simple unit test that now runs in each of our projects. The code, which uses JDepend, is as follows:



package com.theladders.web.dependency;
import com.theladders.test.dependency.AbstractJDependTestCase;
public class CircularDependencyTestCase extends AbstractJDependTestCase
{
public void testForCircularDependencies()
{
jdepend.analyze();
assertEquals("Cycle exists", false, jdepend.containsCycles());
}
}

package com.theladders.test.dependency;
import java.io.File;
import jdepend.framework.JDepend;
import jdepend.framework.JavaPackage;
import jdepend.framework.PackageFilter;
import junit.framework.TestCase;

public abstract class AbstractJDependTestCase extends TestCase
{
protected static final String LADDERS_PACKAGE_PREFIX = "com.theladders";
protected JDepend jdepend;
protected void setUp() throws Exception
{
jdepend = new JDepend();
jdepend.setFilter(new LaddersPackageFilter());
final String[] cps = System.getProperty("java.class.path").split(File.pathSeparator);
for (final String cp : cps)
{
final File f = new File(cp);
if (f.exists() && f.isDirectory())
{
jdepend.addDirectory(cp);
}
}
}
private static final class LaddersPackageFilter extends PackageFilter
{

@Override
public boolean accept(final String packageName)
{
return packageName.startsWith(LADDERS_PACKAGE_PREFIX);
}
}
}



Note: this setup assumes that the code you are testing for dependencies is in directories while the dependencies of that code are in jar or zip files.

With this test (modified for your purposes, of course) running in your continuous integration server, you can be assured that you will know about circular dependencies as soon as they happen.