I am currently struggling with a circular dependency problem when designing my classes.
Ever since I read about the Anemic Domain Model (something I was doing all the time), I have really been trying to get away from creating domain objects that were just "buckets of getters and setters" and return to my OO roots.
However, the problem below is one that I come across a lot and I'm not sure how I should solve it.
Say we have a Team class, that has many Players. It doesn't matter what sport this is :) A team can add and remove players, in much the same way a player can leave a team and join another.
So we have the team, which has a list of players:
public class Team { private List<Player> players; // snip. public void removePlayer(Player player) { players.remove(player); // Do other admin work when a player leaves } }
Then we have the Player, which has a reference to the Team:
public class Player { private Team team; public void leaveTeam() { team = null; // Do some more player stuff... } }
One can assume that both methods (remove and leave) have domain-specific logic that needs to be run whenever a team removes a player and a player leaves a team. Therefore, my first thought is that when a Team kicks a player, removePlayer(...) should also call the player.leaveTeam() method...
But then what if the Player is driving the departure - should the leaveTeam() method call team.removePlayer(this)? Not without creating an infinite loop!
In the past, I'd have just made these objects "dumb" POJOs and had a service layer do the work. But even now I'm still left with that problem: to avoid circular dependencies, the service layer still has link it all together - i.e.
public class SomeService { public void leave(Player player, Team team) { team.removePlayer(player); player.leaveTeam(); } }
Am I over complicating this? Perhaps I'm missing some obvious design flaw. Any feedback would be greatly appreciated.
Thanks all for the responses. I'm accepting Grodriguez's solution as it is the most obvious (can't believe it didn't occur to me) and easy to implement. However, DecaniBass does make a good point. In the situation I was describing, it is possible for a player to leave a team (and be aware of whether he is in a team or not) as well as the team driving the removal. But I agree with your point and I don't like the idea that there's two "entry points" into this process. Thanks again.
In software engineering, a circular dependency is a relation between two or more modules which either directly or indirectly depend on each other to function properly. Such modules are also known as mutually recursive.
There are a couple of options to get rid of circular dependencies. For a longer chain, A -> B -> C -> D -> A , if one of the references is removed (for instance, the D -> A reference), the cyclic reference pattern is broken, as well. For simpler patterns, such as A -> B -> A , refactoring may be necessary.
Avoiding circular dependencies by refactoring The NestJS documentation advises that circular dependencies be avoided where possible. Circular dependencies create tight couplings between the classes or modules involved, which means both classes or modules have to be recompiled every time either of them is changed.
and, yes, cyclic dependencies are bad: They cause programs to include unnecessary functionality because things are dragged in which aren't needed. They make it a lot harder to test software. They make it a lot harder to reason about software.
You can break the circular dependency by adding guards to check if the team still has the player / the player is still in the team. For example:
In class Team
:
public void removePlayer(Player player) { if (players.contains(player)) { players.remove(player); player.leaveTeam(); // Do other admin work when a player leaves } }
In class Player
:
public void leaveTeam() { if (team != null) { team.removePlayer(this); team = null; // Do some more player stuff.. } }
Ben,
I would start by asking if a player can (logically, legally) remove himself from the team. I would say the player object doesn't know what team he's in (!), he is part of a team. So, delete Player#leaveTeam()
and make all team changes occur through the Team#removePlayer()
method.
In the case where you only have a player and need to remove it from its team, then you could have a static lookup method on Team public static Team findTeam( Player player ) ...
I know this is less satisfying and natural than a Player#leaveTeam()
method, but in my experience you can still have a meaningful domain model.
2 way references (Parent -> Child and Child-> Parent) are often fraught with other things, say Garbage Collection, maintaining the "referential integrity", etc.
Design is a compromise!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With