Several well-known adages in Object-oriented design suggest that the dependency structure of a package or module must never contain cycles, or in other words, must orchestrate a Directed Acyclic Graph (DAG).

There are several advantages of eliminating cycles between packages:

Noncompliant Code Example

This noncompliant code example features two different packages named Account and User that consist of the classes AccountHolder and UserDetails, respectively. The class UserDetails extends from AccountHolder because a user is a kind of account holder. The class AccountHolder depends on a few non-static utility methods defined in UserDetails and must declare and use its instance. Likewise, the UserDetails depends on AccountHolder but instead chooses to extend from it. This vicious circle is one recipe for a cyclic dependency.

package Account;
import User.*;
  public class AccountHolder {
    private UserDetails ud;  // Uses a class defined in package User

    final synchronized void depositFunds(String username, double amount) {
      // Use a utility method of UserDetails to check if username exists
      if(ud.exists(username)) { 
        // Deposit the amount
      } 
    }

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

package User;
import Account.*;
  class UserDetails extends AccountHolder {
    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; // 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 dependency is eliminated by ensuring that the AccountHolder does not use an instance of UserDetails, but instead relies on the interface by importing the Bank package (not 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 gives the AccountHolder a solid contract to bank upon. 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.*;  // Import from a third package
class AccountHolder  {	  
  final synchronized void depositFunds(BankApplication ba, String username, double amount) {	
    // Use a utility method of UserDetails to check if username exists
    if(ba.exists(username)) { 
      // Deposit the amount
    } 
  } 
  public final double getBalance(String accountNumber) { 
    // Return the account balance 
    return 1.0; 
  }   
}

package User;
import Account.*; // One way dependency
import Bank.*;    // Import from a third package
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 if user exists
    return true; 
  }
} 

package Implementer;
import Bank.*;
import Account.*;
import User.*;
class BankOperations {
  private BankApplication ba;
  public BankOperations(BankApplication ba) {
    this.ba = ba;
  }

  public void doUserActions() {
    System.out.println(ba.exists("user"));
    System.out.println(ba.getUserBalance("1111"));
  }

  public static void main(String[] args) {
    AccountHolder ac = new AccountHolder(); 
    ac.depositFunds(new UserDetails(), "user", 1.0); // Pass an interface argument
    BankOperations bo = new BankOperations(new UserDetails()); 
    bo.doUserActions(); 
  }
} 

It might appear that the interface BankApplication contains 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' methods polymorphically (such as calling ba.getBalance(), with an overridden implementation of the method in UserDetails).

Risk Assessment

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

Recommendation

Severity

Likelihood

Remediation Cost

Priority

Level

MSC08- J

low

probable

medium

P4

L3

Related Vulnerabilities

Search for vulnerabilities resulting from the violation of this rule on the CERT website.

References

\[[Martin 96|AA. Java References#Martin 96]\] 
\[[Knoernschild 01|AA. Java References#Knoernschild 01]\] Chapter 1: "OO Principles and Patterns, 1.2.5 Acyclic Dependencies Principle"


MSC07-J. Eliminate class initialization cycles      49. Miscellaneous (MSC)      MSC09-J. Carefully design interfaces before releasing them