26. Eliminating Circular Dependency

Part of CS:2820 Object Oriented Software Development Notes, Fall 2020
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

 

Circularity

As a result of documenting the inter-class dependencies in the makefile for the traffic simulator, we ended up triggering some warning messages from make:

[dwjones@fastx02 ~/project]$ make
javac Error.java
javac MyScanner.java
javac Simulator.java
make: Circular Intersection.class <- Road.class dependency dropped.
make: Circular NoStop.class <- Intersection.class dependency dropped.
make: Circular NoStop.class <- Road.class dependency dropped.
javac NoStop.java
javac Source.java
make: Circular Sink.class <- Intersection.class dependency dropped.
make: Circular Sink.class <- Road.class dependency dropped.
make: Circular StopLight.class <- Intersection.class dependency dropped.
make: Circular StopLight.class <- Road.class dependency dropped.
javac Road.java
javac RoadNetwork.java
[dwjones@fastx02 ~/project]$ 

It seems that make is not really happy with makefiles containing circular dependencies. In our case, we have circular dependencies between classes Road and Intersection, and we have circular dependencies between class Intersection and its subclasses.

The Java language itself is not particularly bothered by circular dependencies, but these cause problems in some other object-oriented programming languages. C++, for one, is less tolerant of circular dependencies.

A strategy for Eliminating Circular Dependencies

Consider the following skeleton describing one circular dependency in our road-network simulator:

class Road {
    private final Intersection destination;
    private final Intersection source;

    ... various methods ...
}

class Intersection {
    private final LinkedList  outgoing = new LinkedList 
    protected final LinkedList  incoming = new LinkedList 

    public abstract void addIncoming( Road r, int q );
    public void addOutgoing( Road r ) { ... deleted code ... }
    public Road pickOutgoing() { ... deleted code ... }

    ... other methods ...
}

Each class above contains instance variables that refer to objects of the other classes, and class Intersection contains methods that take parameters of the other class or return instances of the other class.

The key, in Java, to eliminating circularity, is to add some layers of either interfaces or abstract classes. For each cycle, at least one class in the cycle must be broken into two. So if class A and B are in a circular relationship, we can make class AA depend on class B which depends on interface or abstract class A. For the road-network example, we can break up class road as follows:

interface Road {
    ... public method interfaces ...
}

class Intersection {
    private final LinkedList  outgoing = new LinkedList 
    protected final LinkedList  incoming = new LinkedList 

    public abstract void addIncoming( Road r, int q );
    public void addOutgoing( Road r ) { ... deleted code ... }
    public Road pickOutgoing() { ... deleted code ... }

    ... other methods ...
}

class RealRoad implements Road {
    private final Intersection destination;
    private final Intersection source;

    ... public method implementations ...
}

In the above, class RealRoad depends on class Intersection and on class Road, and class Intersection depends only on class Road.

We could not easily split class Intersection because class Road turns relies on the static method Intersection.lookup(), and it uses the addOutgoing(), addIncoming() and enter() methods of Intersection instances, and it also directly accesses the name field. What we can do, however, is split both Road and Intersection:

interface Road {
    ... public method interfaces ...
}

interface Intersection {
    public void addIncoming( Road r, int q );
    public void addOutgoing( Road r );
    public Road pickOutgoing();

    ... other public methods interfaces ...
}

class RealRoad implements Road {
    private final Intersection destination;
    private final Intersection source;

    ... various methods ...
}

class RealIntersection implements Intersection {
    private final LinkedList  outgoing = new LinkedList 
    protected final LinkedList  incoming = new LinkedList 

    // public abstract void addIncoming( Road r, int q );
    public void addOutgoing( Road r ) { ... deleted code ... }
    public Road pickOutgoing() { ... deleted code ... }

    ... other methods ...
}

Java gives you the choice of using interfaces or classes. This choice does not always work the same in other languages. Some languages only have class hierarchies, while others use vastly different models for what Java calls an interface and the corresponding category of class hierarchy.

The influence of C++