Skip to end of metadata
Go to start of metadata

Both The Elements of Java Style [Vermeulen 2000] and the JPL Java Coding Standard [Havelund 2010] require that the dependency structure of a package must never contain cycles; that is, it must be representable as a directed acyclic graph (DAG).

Eliminating cycles between packages has several advantages:

  • Testing and maintainability. Cyclic dependencies magnify the repercussions of changes or patches to source code. Reducing the repercussions of changes simplifies testing and improves maintainability. Inability to perform adequate testing because of cyclic dependencies is a frequent source of security vulnerabilities.
  • Reusability. Cyclic dependencies between packages require that the packages be released and upgraded in lockstep. This requirement reduces reusability.
  • Releases and builds. Avoiding cycles also helps to steer the development toward an environment that fosters modularization.
  • Deployment. Avoiding cyclic dependencies between packages reduces coupling between packages. Reduced coupling reduces the frequency of runtime errors such as ClassNotFoundError. This, in turn, simplifies deployment.

Noncompliant Code Example

This noncompliant code example contains packages named account and user that consist of the classes AccountHolder, User, and UserDetails respectively. The class UserDetails extends from AccountHolder because a user is a kind of account holder. The class AccountHolder depends on a nonstatic utility method defined in the User class. Likewise, the UserDetails depends on AccountHolder by extending it.

package account;
import user.User;
public class AccountHolder {

  private User user;
  public void setUser(User newUser) {user = newUser;}
 
  synchronized void depositFunds(String username, double amount) {
    // Use a utility method of User to check whether username exists
    if (user.exists(username)) { 
      // Deposit the amount
    } 
  }

  protected double getBalance(String accountNumber) { 
    // Return the account balance 
    return 1.0; 
  }
}

package user;
import account.AccountHolder;
public class UserDetails extends AccountHolder {
  public synchronized double getUserBalance(String accountNumber) {
    // Use a method of AccountHolder to get the account balance 
    return getBalance(accountNumber); 
  }
}
 
public class User {
  public boolean exists(String username) { 
    // Check whether user exists 
    return true; // Exists
  }
}

Compliant Solution

The tight coupling between the classes in the two packages can be weakened by introducing an interface called BankApplication in a third package, bank. The cyclic package dependency is eliminated by ensuring that the AccountHolder does not depend on User but instead relies on the interface by importing the bank package (and not by implementing the interface).

In this compliant solution, such functionality is achieved by adding a parameter of the interface type BankApplication to the depositFunds() method. This solution gives the AccountHolder a solid contract to bank on. Additionally, UserDetails implements the interface and provides concrete implementations of the methods while at the same time inheriting the other methods from AccountHolder.

package bank;
public interface BankApplication {   
  void depositFunds(BankApplication ba, String username, double amount);
  double getBalance(String accountNumber);
  double getUserBalance(String accountNumber);
  boolean exists(String username); 
}

package account;
import bank.BankApplication;  // Import from a third package
class AccountHolder  {
  private BankApplication ba;
  public void setBankApplication(BankApplication newBA) {
    ba = newBA;
  }

  public synchronized void depositFunds(BankApplication ba, 
      String username, double amount) {	
    // Use a utility method of UserDetails 
    // to check whether username exists
    if (ba.exists(username)) { 
      // Deposit the amount
    } 
  } 
  public double getBalance(String accountNumber) { 
    // Return the account balance 
    return 1.0; 
  }   
}

package user;
import account.AccountHolder; // One-way dependency
import bank.BankApplication;  // Import from a third package
public class UserDetails extends AccountHolder 
      implements BankApplication {
  public synchronized double getUserBalance(
      String accountNumber) {
    // Use a method of AccountHolder to get the account balance 
    return getBalance(accountNumber); 
  }
  public boolean exists(String username) { 
    // Check whether user exists
    return true; 
  }
}  

The interface BankApplication appears to contain superfluous methods such as depositFunds() and getBalance(). These methods are present so that if the subclass overrides them, the superclass retains the capability of internally invoking the subclass's methods polymorphically (for example, calling ba.getBalance() with an overridden implementation of the method in UserDetails). One consequence of this solution is that methods declared in the interface are required to be public in the classes that define them.

Applicability

Cyclic dependencies between packages can result in fragile builds. A security vulnerability in a package can easily percolate to other packages.

Bibliography

[Havelund 2010]JPL Coding Standard, Version 1.1

[Knoernschild 2002]

§1.2.5, "Acyclic Dependencies Principle"

[Martin 1996]

Chapter 1, "OO Principles and Patterns"

[Vermeulen 2000]The Elements of Java Style

 


8 Comments

    • The NCCE violates DCL00-J. Prevent class initialization cycles. Can we fix this (while still keeping it in violation of this rule)?
    • Why does the CS contain the BankOperations class, but the NCCE doesn't? They should either both contain it or both lack it.
    1. I've addressed my second point by removing the BankOperations class from the CS.

      The first point I've addressed by creating a second User class in the user package in the NCCE. This eliminates the class dependency but preserves the package dependency.

      It's possible that the NCCE before my edits did not technically violate DCL00-J, but it sure looked like it did, and I didn't want to try to explain why it might not.

       

  1. This rule holds for separate java "modules" (aka compilation units/jars), however i don't think it holds within a module (this rule seems to imply that a module will only contain a single package).  for instance, within a given module, there may be a "main" package and a "main.util" package.  the util package may contain utility code for working with the primary classes in the main package, and those primary classes may use the util methods.  i don't think there is anything wrong with this scenario as long as the main and util packages are part of the same jar.  i'm sure you will find many examples of this within the jdk itself (simple example i was looking at the other day in jdk7: java.nio.file.FileSystem and java.nio.file.spi.FileSystemProvider which reference each other in their public API).

    1. First of all, we should probably take out the term 'module'. That term has no meaning in Java. You have classes and packages and jars.

      Second, we could easily address James's comment by s/package/jar/g.

      But I think a better approach would be to s/avoid/minimize/ in the title. IOW cyclic dependencies should be minimized, rather than avoided. We couldn't do that for the Java standard, but we can do it here. And not even the java.nio.* devs can argue with that (smile)

  2. Jame's argument is that there is nothing wrong with cyclic dependencies within packages.  The examples all deal with cyclic dependencies between packages.  The words "minimize" and "avoid" are not that different.  Neither are absolute, so I don't think that is much of a solution.

    s/package/jar/g is one approach, but I disagree that this is easy.  The rule is currently written for dependencies between packages, we would have to produce different examples and do a better job describing what the problem is.

     

     

    1. I've just taken out the word "module" and left everything else as it is.

  3. I have been thinking about this advice all day and the conclusion I have reached is - if we must keep it, it should be called "Reduce tight coupling between classes". Yes, even within the same package.

    I also don't think the NCE shows a cyclic dependency (despite being the original author of the example). Furthermore, the naming of classes needs to be changed for the example to make sense (I can handle that if you all vote to keep this guideline).

    1. I would agree. The only controversial bit here is the notion that this rule is an absolute. The Java library has lots of cyclic dependencies and, as James pointed out, cyclic dependencies can be used in safe code. If this were an absolute rule, it would have gone into our standard. But it's not.