Design for circular dependency
This is the question of how best to handle circular dependency. Let me preface by saying that I think circular dependency is rarely needed, but the code is passed on to me and I cannot help.
Suppose there is a circular dependency on classes with conceptually equal position, i.e. there is no obvious "own" connection between them. How can I handle them gracefully?
Let's take an example where we want to represent a fluid topology of rooms, with a neighbour
reflective property
interface Room {
public void remove(Room r);
}
class Living implements Room {
Room[] neighbour;
public void remove(Room r) {/* implementation */}
}
class Dining implements Room {
Room[] neighbour;
public void remove(Room r) {/* implementation */}
}
Now, obviously, we cannot call remove
to another Room
in the implementation remove
, this is obviously infinite recursion. But then I was left with a few options:
- Make one of
Room
your own, document this fact somehow, and only one typeRoom
has the abilityremove
. - Make a second method
removeSelf
where it never calls the methods of the otherRoom
, thus allowing infinite recursion. - The object has a
Building
higher hierarchy and perform all operations onRoom
And their respective disadvantages
- It is very counterintuitive and imposes an artificial structure when there is none. Moreover, at least one owner
Room
must exist within some neighbor. - Adds a method that users should never call, it sucks as an interface.
- An owner object is required, sometimes also a very inconvenient structure, but
Building
is a coincidence only for the owner object.
So the question is, what's better if circular dependency is somehow inevitable?
We could probably create a class RoomOperator
that contains functions for working with Room
s, but it also suffers the same problems as the method removeSelf
above: it ends up calling a method that is illegal outside RoomOperator
.
source to share
Solution 1
Add functionality to rooms by letting them determine if they need to ask the neighbor to remove them or not, thereby avoiding recursion.
interface Room {
void remove(Room r);
void removeOther(Room r);
}
class Living implements Room {
List<Room> neighbours;
@Override
public void remove(Room r) {
r.removeOther(this);
neighbours.remove(r);
}
@Override
public void removeOther(Room r) {
neighbours.remove(r);
}
}
The method removeOther(Room r)
tells the room to remove only the given room from its list. This contrasts with the method remove(Room r)
, which also makes the room ask for the room given to it to remove it.
This solution preserves the integrity of the interface, if important. I would actually make different rooms by extending the class AbstractRoom
and adding both method implementations there. But since both of your data are the Room
same, it is difficult to know exactly what to do.
If you can change the method to remove(Room r, boolean callback)
, you might be a little confused and only use this method.
I also changed neighbours
to list for convenience only.
Solution 2
Use an external manager (utility) class. This class allows you to eliminate the need to manage rooms with each other.
interface Room {
void remove(Room r);
List<Room> getNeighbours();
}
class Living implements Room {
List<Room> neighbours;
@Override
public void remove(Room r) {
RoomManager.removeRooms(r, this);
}
@Override
public List<Room> getNeighbours() {
return neighbours;
}
}
class RoomManager {
static void removeRooms(Room r1, Room r2) {
r1.getNeighbours().remove(r2);
r2.getNeighbours().remove(r1);
}
}
Again, I add a method to the interface to unify the composition of the rooms. It would be better to use an abstract class here.
source to share
You can add a base class RoomSupport
that manages all dependencies. Other implementations should extend RoomSupport
. Here's the implementation:
abstract class RoomSupport implements Room {
private final Set<Room> neighbours = new HashSet<>();
@Override
public void addNeighbour(Room r) {
if (neighbours.add(r)) {
r.addNeighbour(this);
}
}
@Override
public void remove(Room r) {
if (neighbours.remove(r)) {
r.remove(this);
}
}
}
The implementation takes care of the additions and deletions and no circular dependencies at all.
source to share