0

I am newbie to programming and I am confused how to design an entity objects/classes.

For example I wanted to create an animal kingdom in my application.

Do I need to create a single Base Class (animal) for all the entities or should I create a single class and add a property which describes what the object is.

Example 1:

class Animal
{
}

class Dog : Animal {}
class Cat : Animal {}
class Bird : Animal {}

// etc...

Example 2:

class Animal {
   enum SpecificAnimal { Dog, Cat, Bird, /*etc...*/}

   //declare standard and common properties for all entities
}
2
  • List<ISpeak> vocalList = new List<ISpeak>(); vocalList.Add(dog); vocalList.Add(cat); vocalList.Add(alexa); vocalList.Add(siri); vocalList.Add(politician); foreach (speaker in vocalList) { speaker.Speak(); } Commented Mar 2, 2021 at 8:59
  • This is a brilliant question, it is the essance of OOP to question whether to have subclasses or an enum and the base. Bjarne Stroustrup goes over this problem here: informit.com/articles/article.aspx?p=25003&seqNum=7
    – user362602
    Commented Mar 30, 2021 at 9:03

6 Answers 6

4

Keep it simple

I understand this is likely a toy scenario. And perhaps there is a teacher passing judgement on some criteria of goodness of the design. However, the correct way to do this depends on the requirements, and toy scenarios often have loose requirements compared to the real scenarios.

For that case, I'd favor whatever is easier to extend and easier to understand, which, I believe, would be having multiple classes in this case. You can add more classes if you need them, without having to modify what is already there. Also I think a teacher would want to see you use inheritance. Although, empty classes is often considered a code smell.

There are, of course, more complex approaches that you might need depending on the situation, and I'll mention some of them and some criteria to pick in the nuanced answer blow. Why? Because I don't want to give the impression that there is a correct way to do it that always apply, nor want to dumb down the decision.

However, take this as a reminder to keep it simple. If you don't need an element in your design, don't have it. If you need to pick between equally likely approaches, pick the simple and easy to understand and to extend.


The nuanced answer

Systems need boundaries.

As designer/architect you need to identify/establish a boundary for the system. This is part of the definition of the scope of the project.

If you are, for example, trying to model reality (which is not always the case), you need to consider what things are relevant for the system, and what things aren't.

For example, if you are making an information system for a zoo, you may want to include on what part of the zoo they keep the animal. If you are not making a zoo, that is probably not relevant or doesn't make sense in the system at all.

For another example, if you are modeling street trafic, you might have some properties about the cars, such as their max speed and acceleration. If you are making an information system for a car repair shop, perhaps you are more interested in the repair history of the car, such as what parts have been replaced and so on. That would be out of the scope of the street trafic model.


How to pick between a class and attribute?

Consider, do different animals have different behavior? Is that relevant for the system? does the system treat different animals different?

If the answer is yes, then have a class for each animal. Otherwise treat it like an attribute/property.

For example, cars of different colors behave the same. We do not need to define custom classes for cars of each color, instead we have an attribute/property.

Let us say you have decided that it should be an attribute/property. Your next question is whether or not you can list all possible values during your design process. If you can, then an enum is good solution. Otherwise, consider having kind type for that.

For example you can have an "AnimalSpecies" type that you can initialize with a name, and each animal object would have an "Species" attribute/property of type "AnimalSpecies".


There are other patterns and architectures.

Sometimes a system treats different values of an attribute/property differently, but it does not justify making a new class for each value. In particular, if you have an enum, there could be another part of the system that makes a decision based on the value of that enum. For example, a part of the system shows to user a different picture depending on the kind of animal... Or another part of the system picks different food for different kind of animal, etc...

Here I'd remind you the Single Responsibility Principle. You may have different behavior that depends on the value of that attribute, but they can be different responsibilities, and would belong in different classes.

You can consider using what I called kind type for that, that is, the "AnimalSpecies" can hold the pictures you show, of their food for each case. You might even have to use a different types that have different behavior for the "Species" attribute. See also Strategy pattern.


Oh, by the way, that is all within the realm of Object Oriented Design. I've been assuming a language that favors the Java branch of Object Oriented Programming. Yet, that is not always the case. Know that the patterns work around what the language offers you, and thus you would use different patterns on different language. And while you can use OOD patterns in a language that is not intended for OOP, we can assume there are ways to go about it adapt better to those languages. For example, some languages let you have enum options that have attributes.


Sometimes you need to split your solution into subsystems, each with their own scope and boundary. For example, what picture to show is a presentation concern, but what is their food is not. In that case, either having the enum, or having the "AnimalSpecies" be a simple value that can be passed across boundaries is better. Each subsystem can then pick strategies to handle each case based on that value.


Design is iterative.

Finally I want to say that you do not need to nail the design on the first attempt. Take it as an interactive process. If it is hard to accommodate the requirements in the design you have, refactor it, or consider a different approach. Similarly, new or changing requirements are likely to require changes in your design.

I have given you some considerations that I hope can help you take a decision in a real scenario. However, start with a simple design. If you don't need an element in your design, don't have it. When new requirements come, you will know how to extend your system. And if that requires a refactor, then so be it.

And to reiterate, keep it simple.

2
  • you gave me a very useful information. With that, I guess I will not follow all the standard or proper way of designing for now. Instead, I will make a basic design and move forward. I guess too much learning of online tutorials makes me more confuse when applying it on my project. Thank you.
    – jymskrl
    Commented Mar 2, 2021 at 9:43
  • 1
    @jymskrl I assure that the "proper way" is not a constant. Languages evolve, patterns become obsolete and people invent new ones, and there are always trade-offs. So don't worry too much about the "proper way". In fact, don't let the cargo cult get you. Doing things because it is the "proper way" without understanding will lead to problems. And no, I don't mean study everything before hand. That is Analysis paralysis. I mean learn from experience. Go forth, make your software.
    – Theraot
    Commented Mar 2, 2021 at 11:28
4

Do I need to create a single Base Class (animal) for all the entities or should I create a single class and add a property which describes what the object is.

What you should primarily care about in object orientated design is BEHAVIOUR.

There is a natural tendency take different types of relationships in the real world and automatically assume that you should create a heirachy in your code that uses these relationships, but the only ones you should care about is whether the objects behave the same.

So take your Animal class. What is the behaviour of that class that will also be a behaviour of all child classes.

When it is phrased like that you can start to see that "Animal" as a class probably doesn't give you much benefit, unless there is some behaviour you want all your child classes to posses.

And it is important that ALL child classes have this behaviour, any code that expects Animal should continue to work if passed an instance of a Dog or a Cat.

So you need to make sure that this behaviour is shared universally by all children of the parent class.

A good example of this is you might make a Bird class with a method .fly

You then have a few child classes such as Robin and Penguin.

But you can see the problem, a Penguin can't fly. So calling penguin.fly in code that expects a Bird class won't work

Some people get around this with horrible code that implements .fly in the Penguin class but raises and exception or just does nothing.

But the key insight here is to realize that .fly is not actually universal behaviour for all birds, and behaviour is the key context here when designing OO code.

Another example is the Square Rectangle problem.

In mathematics a Square is a type of Rectangle, when defined in the context of 4 sides. But if you do a class heirachy that has Square as a child class of Rectangle you will run into problems when you implement behaviour.

You scan scale a Rectangle in ways that you cannot scale a Square. To remain a Square you must scale the sides in proportion.

Code that expects a Rectangle to be scalable in only one axis will break when given a Square.

Again you could implement this with confusing code that scales both axis of a Square when your code only scales one axis, but again the key insight is that in the context of behaviour a Square is NOT a Rectangle

So a good rule of thumb is to avoid inherietency until you have identified behaviour that is universal among your objects and only then create an inheritence tree. Don't start off assuming relationships will be there just because in the real world they exist.

2
  • 1
    Excellent answer, the best I've seen so far to these types of questions. Mentions "behavior" in bold in the first sentence, then warns against assuming relationships just because of "real-world". Nice! Commented Mar 3, 2021 at 11:39
  • I agree with @RobertBräutigam and I don't even like OOP all that much. Very good answer!
    – GHP
    Commented Mar 4, 2021 at 18:35
3

There is no one-size-fits-all approach in OOP:

  • Approach 2 is a closed approach: adding a new kind of animals requires to change the class, and to review all its behaviors with the risk of breaking something that perfectly worked before. It’s ok if this property is more descriptive. But in the case of animals, there are a lot of behaviors that are influenced, which would require a lot additional error-prone branching.
  • Approach 1 is an open approach: adding a new kind of animals requires to add a new class and focus on the new behavior, without having to modify your existing classes. It’s called the open-closed principle. It’s a nice design. The problem is just that you're limited to the kind of animals to those foreseen in the code.
  • Approach 3 is more used in games: it's the entity-component-system pattern, which is based on the principle of composition over inheritance: here you decompose behaviors and objects are just sets of behaviors. It’s less intuitive but allows with the same code to let the user configure an animal that barks like a dog, looks like a cat and flies like a bird. If you want to learn more, I recommend the book “Game Coding Complete”, which is full of practical examples.

In summary: avoid 2 as soon as it’s about behaviors; concentrate on 1 to grasp the OOP spirit in an intuitive manner; but go for 3 if you want to enter the game market.

1
  • I'll study approach 3 in the future. Maybe this is really want I want to do but for now I agree with @Theraot. Maybe I'm rushing to learn and too much focusing on designs. I will go with basic design and learn how to refactor along the way.
    – jymskrl
    Commented Mar 2, 2021 at 9:52
0

Example 1 is the preferred option as it provides an actual specifiable type that can be used to describe what sort of objects can be used where, while a field enum cant and you can also use the base class for more specific but still abstract classes to extend from.

eg

class Bird: Animal {}

and

class Pidgeon : Bird{}

allows you to do

someFunction(Bird myBird){}

and still do

someOtherFunction(Animal myAnimal){}

Bird myBird = new Pidgeon()

someFunction(myBird)

someOtherFunction(myBird)

to ensure that only objects inheriting the more specific class can be passed to the function, rather than then having to also introduce another enum to distinguish if it is a bird (or something else), and then ANOTHER enum to then determine its exact type

attempting to create comparable logic with enums to specify the type of animal would lead to runtime checks for compatibility, which are harder to inspect or maintain and are more errorprone

2
  • Yup, I read about the abstract class after learning inheritance. But It's too much for me right now. I can't understand how to use it.
    – jymskrl
    Commented Mar 2, 2021 at 9:56
  • 1
    "preferred" very much depends on intention. If the only intention is data storage, inheritance is not preferred. If the code actually behaves differently based on animal type, then inheritance is preferred. Given that OP is a newcomer, I think it's important to impart that nuanced "A and B each have their purposes" as opposed to a blanket "A is always better than B".
    – Flater
    Commented Mar 2, 2021 at 10:29
0

You've shown us a limited part of the Animal class.

IF this is the only impact your animal type has, i.e. telling you which animal it is, it is better to favor the enum here as the derived classes aren't really being using to their full potential.

However, this is likely not the case. There might be some more animal logic. For example:

public class Animal
{
    public enum SpecificAnimal { Cat, Dog, Bird }

    public SpecificAnimal AnimalType { get; set; }

    public string SaySomething()
    {
        switch(this.AnimalType)
        {
            case SpecificAnimal.Dog:
                return "Woof";
            case SpecificAnimal.Cat:
                return "Meow";
            case SpecificAnimal.Bird:
                return "Tweet";
            default:
                return "...";
        }
    }
}

Here, it would be better to use inheritance, because each animal type has custom logic that is specific to its type.

public class Animal
{
    public virtual void SaySomething()
    {
        return "...";
    }
}

public class Dog : Animal
{
    public override void SaySomething()
    {
        return "Woof";
    }
}

public class Cat : Animal
{
    public override void SaySomething()
    {
        return "Meow";
    }
}

public class Bird: Animal
{
    public override void SaySomething()
    {
        return "Tweet";
    }
}

It may seem overkill for the example I used here, but in the real world your custom logic is going to be significantly more impactful than just returning a hardcoded string.

You don't want to have to stack all dog/cat/bird/... logic into a single class, because it gets way too cumbersome to juggle them all and not get distracted by them at the same time.


Another possible reason to use inheritance is when some animal types require extra information that others don't. For example:

public Color FeatherColor { get; set; }

This obviously only belongs to a bird. If you use a single Animal class, then suddenly all animals could have a feather color, which is wrong.

And you don't want to be using null if you can avoid it. Long advice cut short: avoid null where possible.

Inheritance makes sure that only birds have feathers, but e.g. all animals have an age:

public class Animal
{
    public int Age { get; set; }
}

public class Dog : Animal { }

public class Cat : Animal { }

public class Bird: Animal
{
    public Color FeatherColor { get; set; }
}
0

Start by using enums.

But when implementing behaviour, the code may start to smell.

class Animal {
   enum SpecificAnimal { Dog, Cat, Bird, /*etc...*/}

   void perform()
      if this.type == Bird
         // fly
      if this.type == Cat
         // backflip
}

or

class Animal {
   enum SpecificAnimal {
      Bird { perform() { // fly } },
      Giraffe { perform() { // dance } },
      /*etc...*/
   }

   void perform()
      SpecificAnimal.perform();
}

 ** Pseudocode

Seeing problems? Change approach.

// Dog & Bird Subclasses Animal

Animal animal = Dog();
animal.perform();
// Shake 🐾

animal = Bird();
animal.perform();
// Fly 🦅

animal = Bird(kind = Singing);
animal.perform();
// Sing 🎶

 ** Pseudocode

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.