Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: added CS explanation

...

There are several pros of eliminating cycles between packages:

  • MaintainabilityTesting and maintainability: It is preferable to make a change somewhere and limit the repercussions to as few packages as possible (ideally just one) as opposed to having to monitor or refine numerous packages.
  • Reusability: When a new version of a package is released, clients who reuse it do not have to test their existing code bases for compatibility with other packages that this particular package depends on. Sometimes, the reusable package evolves only to accommodate the changes to packages that it depends on.
  • Releases and builds: Avoiding cycles also helps to steer the development towards an environment that fosters modularization. Owners of different packages are also redeemed from relying on other bleeding-edge packages.
  • Deployment: By resolving the cycles, deployment is simplified, as runtime errors like the infamous ClassNotFoundError, are reduced to a minimum by virtue of the toned down coupling between packages.

Noncompliant Code Example

This noncompliant code snippet features two different packages named Account and User that consist of the classes AccountClass AccountHolderClass and UserClass, respectively. The class UserClass extends from AccountClass AccountHolderClass as one an account can have many users or owners. AccountClass holder can be any kind of user or owner. AccountHolderClass depends upon a few utility methods defined in UserClass and must declare its instance. Likewise, the UserClass depends on AccountClass AccountHolderClass but instead chooses to extend from it. This vicious use is one recipe for a circular dependency.

Code Block
bgColor#ffcccc
package Account;
import User.*;
  class AccountClassAccountHolderClass {
    private UserClass uc;  // uses a class defined in package User
    protected synchronized void depositFunds(String username, double amount) {
      // use a utility method of UserClass to check if username exists
      if(uc.exists(username)) { 
        // deposit the amount
      } 
    }
    protected double getBalance(String accountNumber) { /* return the account balance */ }
  }

package User;
import Account.*;
  class UserClass extends AccountClassAccountHolderClass {
    protected synchronized double getUserBalance(String accountNumber) {
      // use a method of AccountClassAccountHolderClass to get the account balance 
      return getBalance(accountNumber); 
    }
    public boolean exists(String username) { /* check whether user exists*/ }
  }

Compliant Solution

TODO. Outline below. Do not use DRAFTThe tight coupling between the classes in the two separate packages can be weakened by introducing an interface called BankApplication in a third package Bank. The circular dependency is eliminated by ensuring that the AccountHolderClass does not use an instance of UserClass, but instead relies on the interface by importing the Bank package. In this solution, such functionality is achieved by introducing a parameter of the interface type BankApplication to the depositFunds method. This gives the AccountHolderClass a solid contract to bank upon. Additionally, UserClass implements the interface and provides concrete implementations of the methods while at the same time, inheriting the others from AccountHolderClass.

Code Block
bgColor#ccccff
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 AccountClassAccountHolderClass  {	  
  public synchronized void depositFunds(BankApplication ba, String username, double amount) {	
    // use a utility method of UserClass to check if 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.*; // one way dependency
import Bank.*; // import from a third package
class UserClass extends AccountClassAccountHolderClass implements BankApplication {
  public synchronized double getUserBalance(String accountNumber) {
    // use a method of AccountClassAccountHolderClass to get the account balance 
    return getBalance(accountNumber); 
  }
  public boolean exists(String username) { /* check if user exists*/ return true; }
} 


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) {
    AccountClassAccountHolderClass ac = new AccountClassAccountHolderClass(); 
    ac.depositFunds(new UserClass(), "user", 1.0); // pass an interface argument
    BankOperations bo = new BankOperations(new UserClass()); 
    bo.doUserActions(); 
  }
} 

Risk Assessment

Acyclic dependencies between packages can lead to fragile builds. A security vulnerability in any package will easily percolate to several other packages.

Recommendation

Severity

Likelihood

Remediation Cost

Priority

Level

MSC06-J

low

probable

medium

P4

L3

References

Wiki Markup
\[[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"

...