In Rotting Design, I spoke of how software tends to rot over time. When you establish your initial vision for the software’s design and architecture, you imagine a system that is easy to modify, extend, and maintain. Unfortunately, as time passes, changes trickle in that exercise your design in unexpected ways. Each change begins to resemble nothing more than another hack, until finally the system becomes a tangled web of code that few developers care to venture through. The most common cause of rotting software is tightly coupled code with excessive dependencies.
The Problem with Dependencies
Dependencies hinder the maintenance effort. When you’re working on a system with heavy dependencies, you typically find that changes in one area of the application trickle to many other areas of the application. In some cases, this cannot be avoided. For instance, when you add a column to a database table that must be displayed on a page, you’ll be forced to modify at least the data access and user interface layers. Such a scenario is mostly inevitable. However, applications with a well thought dependency structure should embrace this change instead of resist the change. Applications with complex dependencies do not accommodate change well. Instead, with change, the system breaks in unexpected ways and in unexpected places. For this to happen, the module you unexpectedly broke must be dependent on the module that changed.
Dependencies prevent extensibility. The goal of object-oriented systems is to create software that is open for extension but closed to modification. This idea is known as the Open-Closed Principle. The desire is to add new functionality to the system by extending existing abstractions, and plugging these extensions into the existing system without making rampant modifications. One reason for heavy dependencies is the improper use of abstraction, and those cases where abstractions are not present are areas that are difficult to extend.
Dependencies inhibit reusability. Reuse is often touted as a fundamental advantage of well-designed object oriented software. Unfortunately, few applications realize this benefit. Too often, we emphasize class level reuse. To achieve higher levels of reuse, careful consideration must also be given to the package structure and deployable unit structure. Software with complex package and physical dependencies minimize the likelihood of achieving higher degrees of reuse.
Dependencies restrict testability. Tight coupling between classes eliminates the ability to test classes independently. Unit testing is a fundamental principle that should be employed by all developers. Tests provide you the courage to improve your designs, knowing flaws will be caught by unit tests. They also help you design proactively and discourage undesirable dependencies. Heavy dependencies do not allow you to test software modules independently.
Dependencies limit understanding. When you work on a software system, it’s important that you understand the system’s structural architecture and design constructs. A structure with complex dependencies is inherently more difficult to understand.
Dependencies of the worst kind
Excessive dependencies are bad. But cyclic dependencies are especially bad. Cyclic dependencies are manifest in various ways at different levels within a system. It’s also possible that acyclic relationships at one level cause cycles at another.
Types of Cycles
Cycles exist across a variety of entities; notably class, package and JAR. Class cycles exist when two classes, such as Customer and Bill shown here, each reference the other (assume Customer has a list of Bill instances, and Bill references the Customer to calculate a discount amount). This is also known as a bi-directional assocation. It’s a maintenance and testing issue, since you can’t do anything to either class without affecting the other.
Class cycles can be broken a few different ways, one of which is to introduce an abstraction that breaks the cycle, as shown here. Now you can test Bill with a mock DiscountCalculator. Testing Customer, of course, still requires the presence of Bill. This is not a cyclic issue, it’s a different type of coupling issue as Bill is a concrete class, and is fodder for a separate discussion. Introducing DiscountCalculator has broken the cycle between Customer and Bill…but has it broken all cycles?
We don’t intentionally create cyclic dependencies. Instead, they tend to creep into our design. They commonly surface when cyclic or acyclic relationships at one level cause cycles at another. For instance, if Customer and DiscountCalculator are placed in a cust package, and Bill is placed in a billing package, a cyclic dependency between cust and billing exists even though the class structure is acyclic, as shown here. Allocating the cust and billing packages to cust.jar and bill.jar also causes a cycle between the .jars.
To break the cycle, we should move DiscountCalculator to its own package, or the billing package. Simple heh? Well sure…but now toss in a few thousand classes, a few hundred packages, and numerous JAR files, and it’s not so simple to manage anymore.
Fortunately, there are many ways (some easier than others) to manage dependencies and eliminate cycles. Test Driven Development is a great way to manage class cycles assuming we strive to test classes in isolation. JDepend allows you to manage package cycles, either by writing package constraint tests or including JDepend reports within your Ant build script. Jar cycles can be managed using a Levelized Build, where individual jars are built, including only necessary components in the build class path. JarAnalyzer can also be included in your build script, generating a component diagram illustrating the relationship between JAR files, or a dependency report similar to that of JDepend. Maven and Ivy also provide ways to help manage dependencies. And of course, looming on the horizon is OSGi, which may not be ready for widespread enterprise use today, but hopefully will be soon.
Are Cycles Always Bad?
Generally speaking, cycles are always bad! But some cycles are worse than others. Cycles among classes are tolerable, assuming they don’t cause cycles among the packages or JAR files containing them (ie. the classes must be in the same package, essentially encapsulating the design). Cycles among packages may also be tolerable, assuming they don’t cause cycles among the JAR files containing them (again, packages are in the same JAR file). Most important is that we are aware of the relationships existing between the JAR files. In so many cases, we aren’t.