4
\$\begingroup\$

I'm making a small Zoo application, in this question I will cover only the basics - the animals.

I've picked the component-based design over inheritance because the creatures can be quite complex thus I need as much extendability as possible.

I've left most of my questions inside the code in the form of comments at the relevant line, but I will expand a bit more here.

Animal.cs

public class Animal
{
    //Should I use dictionary internally?
    private readonly List<IAnimalComponent> _components;

    public Animal()
    {
        _components = new List<IAnimalComponent>();
    }

    public T GetComponent<T>()
    {
        return _components.OfType<T>().FirstOrDefault();
    }

    //How should I handle multiple components of the same type?
    public Animal AddComponent(IAnimalComponent component)
    {
        if (component == null)
        {
            throw new ArgumentNullException(nameof(component));
        }
        _components.Add(component);
        return this;
    }
}
  1. Should I use dictionary internally? -

    This would be handy when getting/adding the components, as it will become O(1) operation rather than O(n).

    I'm thinking of Dictionary<Type, IAnimalComponent>, but this will cause problems if 2 components of the same type are added, which brings me to my next question.

  2. How should I handle multiple components of the same type? -

    Currently there is no way to obtain multiple components of the same type, but you're able to add them, which doesn't makes much sense. One way to solve it is to have a method which returns all of the components of said type.

IAnimalComponent.cs

The base interface for all of the components.

public interface IAnimalComponent
{
    //Is it okay to be empty?
}

Nameable.cs

Component providing the ability to name objects:

public class Nameable : IAnimalComponent
{
    public string Name { get; }

    public Nameable(string name)
    {
        Name = name;
    }
}

Breedable.cs

Component providing the ability to breed objects and have a gender:

[Flags]
public enum Breeds
{
    Lion = 1,
    Tiger = 2,
}

public enum Gender
{
    Male,
    Female,
}

public class Breedable : IAnimalComponent
{
    public Breeds Breed { get; }
    public Gender Gender { get; }

    public Breedable(Gender gender, Breeds breed)
    {
        Breed = breed;
        Gender = gender;
    }

    //Should I return 'Animal' with attached 'Breedable' component instead?
    public Breedable Propagate(Breedable other)
    {
        if (Gender == other.Gender)
        {
            //Would it be better to just return null?
            throw new InvalidOperationException(nameof(other));
        }
        var genders = new[] {Gender.Male, Gender.Female};
        return new Breedable(genders[new Random().Next(0, genders.Length)], Breed | other.Breed);
    }
}

Feedable.cs

Allows objects to be feed:

public class Feedable : IAnimalComponent
{
    public double FeedingLimit { get; set; }
    public double CurrentFood { get; private set; }

    public bool IsFull => FeedingLimit == CurrentFood;

    private bool _canFeed = true;
    public bool CanFeed
    {
        get => _canFeed && !IsFull;
        set => _canFeed = value;
    }

    public Feedable(double feedingLimit, double currentFood)
    {
        if (feedingLimit < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(feedingLimit));
        }
        if (currentFood < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(currentFood));
        }
        FeedingLimit = feedingLimit;
        CurrentFood = currentFood;
    }

    public Feedable(double feedingLimit) : this(feedingLimit, 0)
    {           
    }

    public bool TryToFeed(double foodAmount)
    {
        if (CanFeed && !IsFull)
        {
            var result = CurrentFood + foodAmount;
            if (result > FeedingLimit)
            {
                return false;
            }
            CurrentFood = result;
            return true;
        }
        return false;
    }
}

Example usage:

class Program
{
    private static void Main()
    {
        Animal motherTiger = new Animal()
            .AddComponent(new Breedable(Gender.Female, Breeds.Tiger))
            .AddComponent(new Feedable(100, 100));

        Animal fatherLion = new Animal()
            .AddComponent(new Nameable("Papa lion"))
            .AddComponent(new Breedable(Gender.Male, Breeds.Lion))
            .AddComponent(new Feedable(100, 100));

        Breedable childBreed = fatherLion.GetComponent<Breedable>()
            .Propagate(motherTiger.GetComponent<Breedable>());

        Animal liger = new Animal()
            .AddComponent(childBreed)
            .AddComponent(new Feedable(100, 50));
        Console.WriteLine(liger.GetComponent<Breedable>().Breed);
        Console.ReadKey();
    }
}

Any critic, suggestions or corrections are welcome.

\$\endgroup\$
  • 1
    \$\begingroup\$ "is it okay for an interface to be empty?" Not really, no. I find it's a smell. An interface defines a contract, so what's the value of a contract without any clauses? \$\endgroup\$ – RubberDuck Jul 19 '17 at 23:00
  • 1
    \$\begingroup\$ I think an empty interface is a smell to the extent that we don't know why it's here. The idea of a "marker interface" is legitimate (see NikitaB answer). For example I want a simple transport mechanism to pass a patchwork of types then I might use a marker-interface-typed list. That said, I agree that in this OP it is smelly. \$\endgroup\$ – radarbob Jul 20 '17 at 15:51
  • \$\begingroup\$ Why is Breed a Flag enum? Can an animal be a tiger AND a lion at the same time? :p Man-Bear-Pig incoming \$\endgroup\$ – IEatBagels Jul 20 '17 at 21:50
  • \$\begingroup\$ @TopinFrassi Yes it can be, that's how hybrids are made. \$\endgroup\$ – Denis Jul 20 '17 at 21:55
5
\$\begingroup\$

Since this question looks like it was based on one of my old answers, I feel obliged to share my opinion on some of your concerns. :)

//Should I use dictionary internally?

Which lookup is faster will depend on average array size and key type. Assuming this is still "work in progress", I don't think it matters at this point, honestly. Once you have a functional world model of your zoo, you can always come back, do the actual profiling and change internal implementation if needed.

//How should I handle multiple components of the same type?

You have to make a decision whether you want to allow that. After all this:

.AddComponent(new Disease("A"))
.AddComponent(new Disease("B"))

can always be refactored into this:

.AddComponent(new Diseases("A", "B"))
//.GetComponent<Diseases>().Add("C");

It's a matter of perspective. If you do allow multiple components of the same type, then yes you'd want to have method that returns all components of given type. You would also want to be able to specify whether or not any given component can have multiple instances (using attributes or some IAnimalComponent.IsUnique property) and throw in AddComponent method accordingly.

//Is it okay to be empty?

So called "marker interface" pattern is a controversial topic. Personally, I'd say that yes, it is fine in this particular case. It is a small price to pay for type safety: you can be sure that you can only call AddComponent with types that were specifically designed for your component system. Not to mention that it might not stay empty forever. Maybe it will turn into MonoBehavior-kind of mess. :) Or maybe you would want it to have a single IAnimalComponent.Target property, that would reference the Animal this component belongs to. Who knows. I say - keep the interface for now.

//Should I return 'Animal' with attached 'Breedable' component instead?

Yes, I think it would make more sense, if you have all the required information to construct new animal. If you don't - you should consider moving this logic to game system that would manage the breeding and that would have access to both Animal instances and to your world state.

//Would it be better to just return null?

I think there should be a separate method that checks, whether two animals can breed, that you would call before calling Propagate method. In this case throwing exception if something goes wrong makes more sense.


P.S. I think Artemis nailed this whole framework-independent-entity-based-system-thingy fairly well. You probably don't need all its complexity, but when in doubt it should be a useful source of inspiration. Check it out.

\$\endgroup\$
  • \$\begingroup\$ It is indeed based on one of your answers, thanks for your insights, pretty convinient that you answered it. :) \$\endgroup\$ – Denis Jul 20 '17 at 17:23
  • \$\begingroup\$ Link to the old answer please? ;-) (@Denis ?) \$\endgroup\$ – t3chb0t Jul 20 '17 at 17:50
  • 1
    \$\begingroup\$ @t3chb0t, here \$\endgroup\$ – Denis Jul 20 '17 at 17:57

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.