3

In the game I am working on there is a Match object and a Player object.

The game is divided into different Matches (basically a lobby). Every Player needs to be in a Match but cannot be in several Matches at once.

Match contains the method List<Player> getPlayers() (list of players connected to the match). Player contains the method Match getMatch() (instance of match the player is connected to).

I have been struggling on where the information that says which Player is in which Match.

  • If the Match keeps a list of the players connected to that match, the Player.getMatch() method would have to loop through a MatchManager.getMatches() method to try to find the match that contains the Player instance.

  • If the Player keeps a reference to the Match it is connected to, the Match.getPlayers() method would have to loop through a PlayerManager.getPlayers() method to find every single Player that is connected to that match.

  • If the Match object keeps a list of connected Players and the Player object keeps a reference to the Match object it is connected to, neither Match.getPlayers() nor Player.getMatch() have to loop through a global list but can just return their reference. However, I feel this is very prone to errors as it could be easy for the references to get de-synced and not match up with the "Player must be in exactly one Match" assertion.

I am unsure which approach would be appropriate. It feels like I'm doing something else wrong with my entire code layout that this becomes a problem and that the entire Manager pattern is a bit of an anti-pattern but I'm not sure how to layout my code.

Any tips or thoughts about how I should be thinking about this (and similar code layout questions) would be greatly appreciated.

Not sure if this is appropriate in softwareengineering but I am not sure where the best place to ask for coding practises is.

9
  • seems appropriate to me. Commented Nov 6, 2016 at 1:39
  • 2
    +1 for the name Vapid Linus. Only a Git would choose a name like that.
    – joshp
    Commented Nov 6, 2016 at 3:55
  • @joshp Not sure if that's a compliment, but I'll take it! Thanks, haha!
    – Vapid
    Commented Nov 6, 2016 at 4:05
  • 1
    I get the feeling you're trying to decide if a Player -> Match ref. should exist based on how problematic constantly iterating is assumed be. I don't disagree w/ the selected answer, code must be written! Just make sure implementation concerns do not overly influence design in how Player is used in other contexts.
    – radarbob
    Commented Nov 7, 2016 at 15:45
  • 1
    If the code is self-contained enough, consider posting it in the Code Review SE and highlighting this question. It may be that this uncertainty is a symptom of a larger design issue (e.g., do you really need to be building this kind of relational model at all?) Commented Nov 7, 2016 at 17:26

5 Answers 5

2

TL;DR;

None. Have a central authorithy do the book-keeping but inject it as a delegate to both players and matches. It would seem as if Match had a list of Players and Player had a reference to its match. We cannot trust players or matches enforcing the 1P-1M rule, do we?

Long answer

I designed a solution which is a class called MatchManager who is responsible for registering and unregistering players into matches enforcing the 1P-1M rule. You can also ask it for a list of players for a given match and what match a given player is registered it.

The interesting part is that MatchManager is a singleton and both players and matches are injected the manager as a delegate to their methods.

That way every operation players or matches do are garanteed to be centralized ans synced.

Obviously if you want this solution to be thread safe you should use thread safe versions of the collection classes used.

Show me the code

==> Match.java <==

package matches;

import java.util.ArrayList;
import java.util.List;

public class Match {

    private MatchManager matchManager;

    public Match(MatchManager matchManager){
        this.matchManager = matchManager;
    }

    public void addPlayer(Player player) throws PlayerAlreadyInAnotherMatchExcepction {
        this.matchManager.registerPlayerInMatch(this, player);
    }

    public void removePlayer(Player player) throws PlayerNotInMatchException {
        this.matchManager.unregisterPlayerFromMatch(this, player);
    }

    public List<Player> getMatchPlayers() throws MatchNotRegisteredException{
        return this.matchManager.getMatchPlayers(this);
    }

}

==> Player.java <==

package matches;

public class Player {

    private MatchManager matchManager;

    public Player(MatchManager matchManager){
        this.matchManager = matchManager;
    }

    public Match getMatch() {
        return this.matchManager.getMatch(this);
    }   
}

==> MatchManager.java <==

package matches;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class MatchManager {

    private static MatchManager instance;

    private MatchManager(){}

    public static MatchManager getInstance(){
        if (instance == null){
            instance = new MatchManager();
        }
        return instance;
    }

    Map<Player,Match> regPlayersToMatches  = new HashMap<Player,Match>();
    Map<Match,List<Player>> regMatchesToPlayers  = new HashMap<Match,List<Player>>();

    public void registerPlayerInMatch(Match match, Player player)
            throws PlayerAlreadyInAnotherMatchExcepction {
        Match m = this.regPlayersToMatches.get(player);
        List<Player> p;
        if ( m == null ){
            this.regPlayersToMatches.put(player, match); 
            p = new ArrayList<Player>();
            p.add(player);
            this.regMatchesToPlayers.put(match, p);
        } else {
            throw new PlayerAlreadyInAnotherMatchExcepction();
        }
    }

    public void unregisterPlayerFromMatch(Match match, Player player)
            throws PlayerNotInMatchException {
        List<Player> p = this.regMatchesToPlayers.get(match);       
        if (p==null){
            throw new PlayerNotInMatchException();
        }
        if (p.contains(player)){
            throw new PlayerNotInMatchException();
        }
        this.regPlayersToMatches.remove(player);
        this.regMatchesToPlayers.remove(match);
    }

    public boolean isPlayerInMatch(Match match, Player player){
        List<Player> p = this.regMatchesToPlayers.get(match);
        if (p==null){
            return false;
        }
        return (p.contains(player));    
    }

    public List<Player> getMatchPlayers(Match match) throws MatchNotRegisteredException{
        List<Player> p = this.regMatchesToPlayers.get(match);
        if (p==null){
            throw new MatchNotRegisteredException();
        }
        return new ArrayList<Player>(p);
    }

    public Match getMatch(Player player) {
            return this.regPlayersToMatches.get(player);
    }
}

==> PlayerAlreadyInAnotherMatchExcepction.java <==

package matches;

public class PlayerAlreadyInAnotherMatchExcepction extends Exception {

}

==> PlayerNotInMatchException.java <==

package matches;

public class PlayerNotInMatchException extends Exception {

}

==> MatchNotRegisteredException.java <==

package matches;

public class MatchNotRegisteredException extends Exception {

}

Note: I didn't use interfaces to keep the answer shorter. If you want to comply with the DIP, write interfaces (or abstract classes) to keep dependencies at bay.

2
  • In my case, I already have a MatchManager (keeps a list of all matches) and a PlayerManager (keeps a list of all players). Would this still go in MatchManager or maybe a third one? How'd you deal with that?
    – Vapid
    Commented Nov 9, 2016 at 18:11
  • My manager also keeps a list of all matches, so you can adapt this solution implementing the methods I suggest which will be delegeted functions to players and matches. The existence of a PlayerManager do not affect my proposed solution. Commented Nov 10, 2016 at 12:46
4

In the abstract, the constraint is on the player, so I would have the match hold a list of players, and make the players responsible for joining the match. The match simply needs to know the joined players.

If, at some point the, you can schedule future matches, this would allow the players to still control the one match at a time constraint, and the MatchPlay join logic wouldn't have to change.

Edit:

Regarding having both match and the player have references, this may be the most useful, especially if there are hundreds or thousands of players in a match, and finding a specific player is a common need for match processing.

To expand a little further. I am not sure what, if anything it persisted. If there is persistence, then I'd keep a list of players for each match, the player (assuming it is durable object) would probably keep a list of MatchPlay objects. The MatchPlay would be a match reference with a results/status object (durable reference). Then you'd be able to display analytics about the player, track status, show upcoming and historical matches for the player.

Further, if joining a match had some MATCH based qualifications (min/max rating, paid token, invitation), then the join process initiated by the player would would be more a join REQUEST. The player's constraints may filter join-able matches, while the MATCH's constraint would affect the join request.

And if the concept of match result is baked in, leaving the reference to the match in the player shouldn't be a concern.

Of course, this may be over engineered for a two - four player non-networked coin-op arcade game, hah.

1
2

What you're describing is a bidirectional one to many relationship.

If you have a database you can sidestep this by simply modeling this in there and looking it up with a query. If, however, you want to maintain this with a data structure you're either going to be looping or maintaining a shadow data structure that can become inconsistent if not maintained.

I'm in favor of the Player keeping a reference to the Match simply because enforcing the "Player must be in exactly one Match" assertion becomes a simple test for null.

Rejecting looping could be a premature optimization. Test how many players and matches must be involved before you find the responsiveness intolerable.

If you decide to reject looping then we're stuck with Player keeping a reference and Match keeping a shadow list. Beware, this is a circular reference. Be careful how you traverse.

How to keep the shadow list consistent? Update it when, and only when, the reference is updated. Keep Matches update method accessible only to Player if you can. Then something like this takes care of maintaining consistency:

class Player {
    ...
    void setMatch(Match match){
        this.match.removePlayer(this);
        this.match = match;         
        this.match.addPlayer(this);
    }
}

So long as Player keeps this.match from being null and nothing else messes with add/remove player this should keep things consistent. But some well placed assertions can help ensure this.

3
  • 1
    If you have a database you can sidestep this by simply modeling this in there and looking it up with a query. If, however, you want to maintain this with a data structure... Do not put business rules in the data store (I don't think this is what @CandiedOrange meant). It's nice when a DB relation model maps directly to the business model, but the business model must incorporate all the rules. Otherwise it is undesirable coupling, makes it harder to re-use business classes, and is a fault zone of future re-work.
    – radarbob
    Commented Nov 7, 2016 at 15:13
  • In the picky department: setMatch is a phrase heard in tennis domain terminology, e.g. "game, set, match!" Perhaps assignMatch()?
    – radarbob
    Commented Nov 7, 2016 at 15:23
  • @radarbob "game", "set", and "match" are three different (but related) things in tennis. There is no such thing as a "set match" (although a potentially decisive set could be called a "match set"), and setX is a very common pattern, so even if tennis is in the program's domain I think the risk for confusion is minimal. Commented Nov 7, 2016 at 17:05
1

I am going to surmise that your true question is "how do I decide when to make a relationship between two objects bi-directional, and if I choose to make it one directional, how do I decide which way?"

The answer to this question lies in your clearly defined requirements. If your requirements are not clear, then you are wasting your time trying to compensate for both cases, go back to the model and make sure it is crystal clear.

In almost every case, you want relationships to be one directional since this is the easiest to code and comprehend. If the requirements really do demand a two way relationship, it may be best to abstract out the relationship itself as a third object and give each object a one way relationship to it.

In some cases, you find that you actually don't need a relationship modelled by objects at all. I think this might suit your case. What you are trying to allow for by giving matches and players a relationship is the query for information regarding how matches and players line up. If you abstract this out as a service which takes a player id or a match id and simply gives you the information you are requesting, why do players and matches ever need to know about each other?

Right, so you know how to program, and you know how to implement all possible scenarios, and you know there are a variety of answers to the questions posed, but your real question concerns how to make the decision in any given scenario. What is the best way given the project you are working on? The simple answer is you need to know your model and your requirements, and if you are stumped on an issue like this, it is because your model or your requirements are not as clear as they could be. Go back and work out the model.

If you want to learn more I recommend Domain Driven Design by Eric Evans. If you haven't read it, read it. It may not address your situation exactly, but the principles you pick up from it will serve you in technology forever. If you have read it, read it again, because it addresses certain ways of thinking that strike at the very heart of your real concern:

I am unsure which approach would be appropriate. It feels like I'm doing something else wrong with my entire code layout that this becomes a problem

1
  • Thank you for the general advice! This is something I needed. Will definitely check out the read. Cheers!
    – Vapid
    Commented Nov 9, 2016 at 18:12
0

In my opinion, the match should keep a list of players. That way, you can have history and statistics in terms of which player played which match.

Of course, you must have a validation in place to ensure that a player cannot play two matches simultaneously, but that kind of logic should be placed in some sort of arbitrator class that has information both on players and the matches (e.g. League).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.