HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Automatically update one-to-many bi-directional relationship

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
updateautomaticallydirectionalonemanyrelationship

Problem

In an example game engine server, the game world could be represented by a World object. The game might have multiple game worlds and a player who is in a world only needs to receive position data of units in that world.

class World
{
    public List Units { get; set; }
}


However a piece of code also needs to be able to look up what world a unit is in easily, so the unit object itself keeps track of a world reference.

class Unit
{
    public World World { get; set; }
}


This works for lookup, but quickly becomes problematic when changing data when the programmer isn't aware of the relationship between objects going on, so I changed Units in World to be readonly and have the following code in Unit.

public virtual World World
{
    get { return _world; }
    set
    {
        // If already this, don't do anything
        if (value == _world) return;

        var oldWorld = _world;
        _world = value;

        if(oldWorld != null) oldWorld.UpdateUnitEntry(this);
        _world.UpdateUnitEntry(this);
    }
}


This works, but it feels like there's a better way to do this. Especially as I add more stuff that needs to be linked the same way (a World also has Structures and Players, and Structures maintain a list of Units as well), a lot of repeated functionality comes in. As well when I remove a unit from the game, it continues to be kept alive by this, so I have to remember to set the unit's structure to null every time (which I already forgot multiple times, resulting in weird bugs). Is there a better way to achieve this one-to-many relationship without manually updating both sides?

Solution

The best way to accomplish this is to have each World, Structure, and anywhere else a unit can move to inherit an interface IEnterable, which looks like

public interface IEnterable
{
    void Enter(Unit unit);
    void Leave(Unit unit);
}


Your unit would then have this as a property, and a MoveTo(IEnterable location) method

public class Unit
{
     // ...

     public IEnterable CurrentLocation { get; set; }

     // ...

     public void MoveTo(IEnterable location)
     {
         if (CurrentLocation != null)
         {
              CurrentLocation.Leave(this);
         }

         location.Enter(this);
     }
}


The Enter and Leave would handle all the logic for the Unit movement.

If you wanted to get more fancy, you could have a Movement class that could describe the movement a little more accurately.

public class Movement
{
     private readonly IEnterable _destination;

     public Movement(IEnterable destination)
     {
         if (destination== null) throw new ArgumentNullException("destination"); 

         _destination = destination;
     }

     public MoveResult MoveUnit(Unit unit)
     {
         if (unit == null) throw new ArgumentNullException("unit");

         var currentLocation = unit.CurrentLocation;
         if (currentLocation  == _destination) return MoveResult.SameLocation;

         if (!IsValidMove(currentLocation, _destination) return MoveResult.InvalidMovment;

         currentLocation.Leave(unit);

         _destination.Enter(unit);
     }
}


This will get the movement logic out of the Unit class because a unit should not care about how it has to move, just that it has.

I might be missing something, but my main point is to use an interface for each object that a Unit can enter, and the ease of moving a Unit becomes much better.

Code Snippets

public interface IEnterable
{
    void Enter(Unit unit);
    void Leave(Unit unit);
}
public class Unit
{
     // ...

     public IEnterable CurrentLocation { get; set; }

     // ...

     public void MoveTo(IEnterable location)
     {
         if (CurrentLocation != null)
         {
              CurrentLocation.Leave(this);
         }

         location.Enter(this);
     }
}
public class Movement
{
     private readonly IEnterable _destination;

     public Movement(IEnterable destination)
     {
         if (destination== null) throw new ArgumentNullException("destination"); 

         _destination = destination;
     }

     public MoveResult MoveUnit(Unit unit)
     {
         if (unit == null) throw new ArgumentNullException("unit");

         var currentLocation = unit.CurrentLocation;
         if (currentLocation  == _destination) return MoveResult.SameLocation;

         if (!IsValidMove(currentLocation, _destination) return MoveResult.InvalidMovment;

         currentLocation.Leave(unit);

         _destination.Enter(unit);
     }
}

Context

StackExchange Code Review Q#43644, answer score: 7

Revisions (0)

No revisions yet.