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 type Room

    has the ability remove

    .
  • Make a second method removeSelf

    where it never calls the methods of the other Room

    , 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

.

+3


source to share


2 answers


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.

0


source


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.

0


source







All Articles