Package Depends On Less Stable Package
ID |
java.package_depends_on_less_stable |
Severity |
high |
Remediation Complexity |
hard |
Remediation Risk |
medium |
Remediation Effort |
high |
Resource |
Reliability |
Language |
Java |
Tags |
code-style, reliability |
Description
Reports a package that depends on another in-project package which is less stable than
itself. Stability is measured by instability I = Ce / (Ca + Ce), where Ce is the number of
in-project packages the package depends on (efferent coupling) and Ca the number that depend on
it (afferent coupling). A dependency edge is flagged when the depending package’s I is lower
(more stable) than the depended-on package’s I (less stable) by at least minInstabilityDelta.
The issue is reported on the import statement that creates the edge — specifically the first
such import in the project, ordered by file path and line.
Rationale
A package many others rely on but that itself depends on little is stable: changing it is risky because the change ripples out to every dependent. A package that depends on much but that few others rely on is unstable: cheap to change, since little breaks when it does.
A stable package that depends on an unstable one inherits that instability — every time the unstable package changes, the stable one (and everything that in turn depends on it) is at risk of needing to change too, defeating the whole point of being stable. Robert C. Martin’s Stable Dependencies Principle states the rule directly: a package should only depend on packages that are as stable as, or more stable than, itself.
The violation needs at least two packages, so it cannot be shown in a single file. These are two source files of the same project:
// ---- src/com/example/core/Repository.java ----------------------------------------------------
// Five in-project packages depend on com.example.core (Ca = 5); it depends on one (Ce = 1).
// I = 1 / (5 + 1) = 0.17 -- stable, and expensive to change.
package com.example.core;
// FLAW -- reported on this import. core (I = 0.17) depends on experimental (I = 0.75): a delta
// of 0.58, well over the 0.20 threshold. The five packages that rely on core now inherit the
// volatility of one that nothing else relies on.
import com.example.experimental.DraftCache;
public interface Repository {
DraftCache cache();
}
// ---- src/com/example/experimental/DraftCache.java --------------------------------------------
// Only com.example.core depends on com.example.experimental (Ca = 1); it depends on three
// in-project packages (Ce = 3). I = 3 / (1 + 3) = 0.75 -- unstable, and cheap to change.
package com.example.experimental;
import com.example.io.Blob;
import com.example.net.Http;
import com.example.util.Clock;
public class DraftCache {
// ...
}
Reverse the two and nothing is reported: a package with I = 0.75 depending on one with I = 0.17 is the direction the principle asks for.
Remediation
-
Invert the dependency. Have the less stable package implement an interface the more stable one defines, so the dependency arrow points toward stability instead of away from it.
-
Extract a shared abstraction. Move the piece the stable package actually needs into a package at least as stable as it is, and depend on that instead.
-
Reconsider the package boundary. A stable package that keeps needing something from an unstable one may not have been split along the right seam.
Configuration
| Property | Default | Meaning |
|---|---|---|
|
|
Minimum |
What is not reported
-
Parent/child edges.
com.foo → com.foo.bar, in either direction, is how Java code is laid out, not a statement about which of the two is expected to change more often. -
Edges below
minInstabilityDelta. -
Edges to packages outside the scanned project. Only in-project packages are scored at all; the instability of a third-party library cannot be measured from this code base.