3

I have a ecosystem simulation where animals move and collide with each other. Here is how I handle collisions:

public void HandleCollisionBetween(Animal a, Animal b)
    {
        if (a.GetType().Name == b.GetType().Name) { // same type collision
            bool fishCollision = a.GetType().Name == typeof(Fish).Name;
            if (fishCollision) {
                AddAnimal(new Fish());
            }
        }
        else { // bear-fish collision
            Bear bear = a.GetType() == typeof(Bear) ? a as Bear : b as Bear;
            Fish fish = a.GetType() == typeof(Fish) ? a as Fish : b as Fish;

            bear.Eat(fish);
            RemoveAnimal(fish);
        }
    }

This code is a part of my ecosystem class. It was suggested that I take this code into a separate class, implement a interface and use it instead. This way it becomes testable. The problem is: whenever two animals collide, I need to either add or remove a animal from the ecosystem. All I can think of is this:

interface ICollisionHandler {
    void HandleCollisionBetween(Animal a, Animal b, Ecosystem ecosystem);
}

But this reference to ecosystem just doesn't feel right. I would greatly appreciate any help and information that can help me improve on these kind of things.

8
  • 1
    This is the double dispatch problem. Nov 15, 2019 at 14:23
  • The posted code fails OCP as you would have to keep changing HandleCollisionBetween for every new animal type that you develop. The solution to this brings with it a whole array of questions: what types of interactions exist, does a non-interaction (e.g. collision with animals that do nothing with each other) exist, can interactions be reusably defined, how do you handle different interactions between the same animal types (e.g. shark + goldfish = eating, shark + shark = reproducing), does the order (a-vs-b or b-vs-a) matter, ...?
    – Flater
    Nov 15, 2019 at 14:48
  • I assume bear + bear also reproduces? Are you expecting to develop more animal types, and more interactions? Is it possible you might at some point implement same-type-eating? The overall point I'm trying to make here is that you cannot decide on the architecture until you have a full grasp on the requirements. As the question stands, there is a lot of information missing before any reasonable decision can be made.
    – Flater
    Nov 15, 2019 at 15:24
  • @Flater thank you for help. Two types of interactions exist: bear + fish = eating, fish + fish = reproducing. I believe those can be defined in a reusable way, though I am not clear as to how, yet. A non-interaction exist between two bears. The order (a-vs-b or b-vs-a) does not matter. I am not expecting to develop more animal types or more interactions. This is a practise project. The requirements will not change. Nov 15, 2019 at 15:30
  • @SlavaLogos: Thank you for the input. Since you mention this is a practice project, do you expect an answer that applies in cases where there would be more animal types or interactions? Because as it stands, such a solution is non-trivial and arguably unwarranted overengineering, given the very limited range of options you're trying to account for.
    – Flater
    Nov 15, 2019 at 15:34

1 Answer 1

0

So right off the bat, I find that when you are cross-comparing a lot of classes, one approach is to back up and ask yourself if the classes you have written should be recomposed as DATA, not as classes, at least for the point of contact in your app where they intersect. So to begin with, I'm going to give you an example that reduces the Bear/Fish classes down to one base class, Animal, with the bear-or-fosh details packed inside as data. However, you could take this example further and have a bear and fish class that inherits from Animal, but for the purpose of determining the collision outcome, I do not feel that having them as classes adds much value. Here's my building blocks.

  • An EcoSystem class that knows what animals are present in the environment and has the public function to be called when a collision is desired
  • An Animal base class that allows instances to understand their species (bear or fish) as well as a unique identifier to help track them in the ecosystem
  • A CollisionResolver class who's sole reason to change is to handle mapping new animal conflicts, if/when new animal types are added.

I'm not a big believer in INTERFACES FOR EVERYTHING, but I will give you this example, which breaks the problem into the above chunks:

public class Animal
{
    public string Identifier { get; } 
    public string SpeciesName { get; } 

    public Animal(string animalName)
    {
        SpeciesName = animalName;
        Identifier = Guid.NewGuid().ToString();
    }
}

public class AnimalCollisionResolver
{
    public ICollisionResult ResolveCollision(Animal a, Animal b)
    {
        // Handle the easy case -> reproduction of same species
        if (a.SpeciesName == b.SpeciesName)
            return new AddNewAnimalCollisionResult { NewAnimalToAdd = new Animal(a.SpeciesName) };

        // Handle handle harder case, how to determine which ones eat the others, this will be as complex as you need it

        var animals = new[] { a, b };
        var names = animals.Select(_ => _.SpeciesName);

        // Bear-eats-Fish mapping here
        if (names.Contains("Bear") && names.Contains("Fish"))
        {
            return new RemoveSpecificAnimalCollisionResult
            {
                IdentifierOfAnimalToRemove = animals.Single(_ => _.SpeciesName == "Fish").Identifier
            };
        }

        throw new NotImplementedException("I need to map out the rest of the collision resolutions here.");
    }
}

// This could be an abstract base class and the code would not change, FYI
public interface ICollisionResult { }

public class AddNewAnimalCollisionResult : ICollisionResult
{
    public Animal NewAnimalToAdd { get; set; }
}

public class RemoveSpecificAnimalCollisionResult : ICollisionResult
{
    public string IdentifierOfAnimalToRemove { get; set; }
}

public class Ecosystem
{
    public List<Animal> CurrentAnimals { get; set; }

    public AnimalCollisionResolver CollisionResolver { get; set; }

    public Ecosystem()
    {
        // seed the ecosystem here
        CurrentAnimals = new List<Animal> {
            new Animal("Bear"),
            new Animal("Bear"),
            new Animal("Fish"),
            new Animal("Fish"),
        };

        CollisionResolver = new AnimalCollisionResolver();
    }

    public void HandleCollision(Animal a, Animal b)
    {
        var result = CollisionResolver.ResolveCollision(a, b);

        if (result is AddNewAnimalCollisionResult)
        {
            var addResult = (AddNewAnimalCollisionResult)result;
            CurrentAnimals.Add(addResult.NewAnimalToAdd);
        }

        else if (result is RemoveSpecificAnimalCollisionResult)
        {
            var removeResult = (RemoveSpecificAnimalCollisionResult)result;
            CurrentAnimals = CurrentAnimals.Where(_ => _.Identifier != removeResult.IdentifierOfAnimalToRemove).ToList();
        }

        else
        {
            throw new Exception("Unknown ICollisionResult here");
        }

        PrintOutTheCurrentAnimals();
    }

    public void PrintOutTheCurrentAnimals()
    {
        foreach (var animal in CurrentAnimals)
            Console.WriteLine($"{animal.SpeciesName} {animal.Identifier}");
        Console.WriteLine("");
    }
}

I'm not sure how you're declaring the animals and deciding who collides with who, but here's an example to show the basics:

var ecoSystem = new Ecosystem();
ecoSystem.PrintOutTheCurrentAnimals();

var allBears = ecoSystem.CurrentAnimals.Where(_ => _.SpeciesName == "Bear");
var bear1 = allBears.Take(1).Single();
var bear2 = allBears.Skip(1).Take(1).Single();
var fish1 = ecoSystem.CurrentAnimals.First(_ => _.SpeciesName == "Fish");

ecoSystem.HandleCollision(bear1, fish1);

ecoSystem.HandleCollision(bear1, bear2);
1
  • Thank you @Graham. This is just what I need. I would never think of returning something like ICollisionResult. This opened my eyes to a new possibility. Nov 16, 2019 at 6:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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