The Package Dependency Inverter (PDI)

Purpose

to show existing dependencies between packages; to replace call dependencies between packages with reversely directed subtype dependencies; to provide tool support for the Dependency Inversion Principle

System requirements

Rationale

Java programs are organized in packages. If these packages are viewed as modules, dependencies between them should be as small as possible (low coupling). Since packages are not themselves related, all dependencies between them are by way of their contained members (classes and interfaces, in the following collectively refered to as types).

In Java, there are two kinds of dependencies between types: dependencies by use and dependencies by subtyping. The former materialize in the form of member declarations (where the declaration of a member in one type references another type), the latter in the form of type declarations (where the declaration of one type references another as its supertype).

To minimize dependencies between packages (by use of suitable refactorings such as Infer Type or Inject Dependency), the dependencies must be known. The PDI is an analysis tool that derives exisiting dependencies from a program source and depicts them in diagramatic form. In addition, it lets one replace uses dependencies by subtype dependencies, effectively inverting the direction of dependency.

Background

A dependency from package p to package q exists if the removal of q requires changes in p before p can be compiled without errors. Package p is then called dependent on q.

In Java, two kinds of dependencies exist: uses dependencies, and subtype dependencies. A use dependency of type A on type B exists if A references B in the declaration of one of its members, as in

class A {
   B b;
}

A subtype dependency of type A on type B exists if A declares being a subtype of B, as in

class A extends B {}

In addition to these direct dependencies, there are also indirect dependencies: if a package p is indirectly dependent on q, changes in p may be necessary to make p compilable after q has been deleted. Further details can be found here. Note that while use dependencies can be cyclic, this is impossible for subtype dependencies.

If dependencies between types cross package boundaries, they are lifted to the package level: the package with the depending type depends on the package of the type being depended upon. Dependencies accross packages typically require an import statement in the depending package (or a package name qualified reference to the type on which is being depended).

Note that although Java package naming suggest a hierarchy of packages, nesting of packages does not have any meaning. In particular, dependency of or on a package is not formally inherited to subpackages or superpackages. However, the nesting of packages allows a compaction of views (by not showing subpackages); in order not to lose dependency information, the PDI lifts depencies from subpackages to superpackages in these cases.

Usage

To invoke the PDI, select "Package Dependencies..." from the context menu of a project in Eclipse's Package Explorer.

The following dialog allows the selection of the scope in which dependencies are to be found:

By default, the PDI searches for dependencies between all packages of the project it was invoked on, more precisely from all packages to all packages of that project. However, it is possible to limit the scope of the search: both the packages from and to which dependencies should be found can be constrained by selecting "...selected packages" and the desired packages. For convenience, the current selection can be inverted and the selection from one table can be adopted for the other by clicking the corresponding arrow button.

In addition, the PDI allows the examination of dependencies between the project it was invoked on and packages of other projects in the current workspace. The packages of the projects selected will be added to the scope tables, wherein the search scope can be narrowed further in the way described above.

Once selection is finished, "Show Dependencies" brings you to the Viewer. (Note that depending on the size of the search scope this may take some time.)

The Viewer offers two different diagram types:

  1. The Bow View(er) uses nested boxes to represent Java packages and their naming hierarchy. One box is inside of another if and only if the inner is a subpackage of the outer.
  2. The Tree View(er) displays the package hierarchy of projects in tree form. The nodes of the trees represent packages; edges between them exist if and only if one is a direct subpackage of the other.

In both diagram types, the types contained in a package can be shown by clicking on "Show Types...". Also, subpackages can be hidden by clicking on the triangle trailing the package name.

Dependencies between packages are shown using arrows between their corresponding boxes. Use dependencies are shown by red UML dependency arrows, and subtype dependencies are shown by green UML generalization arrows. Both can be hidden, by clicking the corresponding button. By default, only direct dependencies are shown. Clicking on "Only Cyclic" reduces shown dependencies to cyclic ones (note that this rules out all subtype dependencies).

Selecting a dependency (by clicking on it) fills the lower pane with an expandable tree containing the dependencies represented by the arrow. The leaves of this tree are the lines in code that set up the dependency (with the exact occurence highlighted). Selecting a package fills the lower pane with all dependencies of that package.

The PDI allows certain use dependencies to be replaced by subtype dependencies, simply be clicking the "Invert Dependency" button. As a result of this, the "foreign" type is replaced by a locally defined new interface and the foreign type is made to extend or implement this interface, thereby reversing the dependency between packages. One precondition to the complete reversal is that no constructors of the class which is being depended upon are called from the package; if this is the case, the Inject Dependency refactoring (not yet available) must be used instead. Another precondition is that no field access must exist (since fields cannot be parts of interfaces); this can be fixed by using the Encapsulate Field refactoring prior to dependency inversion.

The following example illustrates what can be done:

package a;
class A {
  b.B someField;
  void doSth() {
    someField.someMethod();
  }
}
package b;
class B {
  public void someMethod() {}
}

The reference to B in A causes a dependency from a to b. However, inverting this dependency is possible: introducing an interface I which contains the method someMethod(), inserting it into A's package a, changing the type of someField to I and letting B implement the interface, leads to the following code:

package a;
class A {
  I someField;
  void doSth() {
    someField.someMethod();
  }
}
package a;
interface I {
  public void someMethod();
}
package b;
class B implements a.I {
  public void someMethod() {}
}

Note that A does not reference B any more and thus the dependency from a to b has disappeared. B references I by implementing it, which makes b dependent on a.

Note that inverting dependency can be used for breaking up dependency cycles.

After a successful refactoring, a re-search for dependencies in the changed packages takes place and the Viewer panel is updated.

Known issues

Installation

  1. Make sure that GEF is installed; if not, install it as described here.
  2. Copy this and this jar into Eclipse’s plugin directory (see Known Issues for possible conflicts).
  3. Restart Eclipse.

Publications

Team