CodeGym /Courses /C# SELF /Building Class Hierarchies

Building Class Hierarchies

C# SELF
Level 20, Lesson 4
Available

1. Introduction

Imagine a world without hierarchies: thousands of Person, Animal, Vehicle classes, all totally separate. No wonder programmers in that world wouldn't even make it to lunch—they'd be totally lost! In real projects, we often need objects that can do something in common (like, all animals can move), but each has its own quirks (fish swim, birds fly).

Class hierarchies let us express relationships between entities, so programming becomes creative work, not just an endless battle with copy-paste.

Let's look at an example. Say we have a base class Animal. All animals can make a sound. But only cats meow, dogs bark, and parrots can even tell a couple of jokes. We want to express this in code, hierarchically.

Base class


public class Animal
{
    public string Name { get; set; }

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

    // Base method: can be overridden in children
    public virtual void Speak()
    {
        Console.WriteLine("Animal makes some kind of sound...");
    }
}

Here we added the virtual keyword to the Speak() method. It's like a note: "Hey, child classes, if you want—you can override this method."

Building the hierarchy: derived classes

Now let's have a Cat class that inherits from Animal:


public class Cat : Animal
{
    public Cat(string name) : base(name) { }

    // Override Speak—cats can't just roar!
    public override void Speak()
    {
        Console.WriteLine($"{Name} says: Meow!");
    }
}

And a Dog class:

public class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public override void Speak()
    {
        Console.WriteLine($"{Name} says: Woof!");
    }
}

And what if we have a regular animal that can't talk? Then you can just use the base class, without overriding anything.

Visualization — hierarchy tree


.               Animal
                /   \
             Cat    Dog
  • Animal — base class
  • Cat, Dog — child (derived) classes

2. Let's write code that uses this hierarchy

Let's keep working on our console app.

Say we have a collection of animals and we want each one to say something unique:

Animal[] zoo = new Animal[]
{
    new Cat("Whiskers"),
    new Dog("Rex"),
    new Animal("Mysterious creature")
};

foreach (Animal animal in zoo)
{
    animal.Speak();
}

Expected output:

Whiskers says: Meow!
Rex says: Woof!
Animal makes some kind of sound...

See, thanks to hierarchy and polymorphism (we'll dig into it soon, but basically—it calls the right version of the method depending on the actual object type), your app becomes flexible and easy to extend.

3. Adding new methods and fields

Okay, we've got "talking" down. But all animals being the same is boring. For example, a cat might have nine lives, and a dog can fetch a stick.

Adding unique behavior

In a child class, you can add your own methods and fields:


public class Cat : Animal
{
    public int Lives { get; private set; } = 9;

    public Cat(string name) : base(name) { }

    public override void Speak()
    {
        Console.WriteLine($"{Name} says: Meow! I have {Lives} lives.");
    }

    public void LoseLife()
    {
        if (Lives > 0)
        {
            Lives--;
            Console.WriteLine($"{Name} lost one life. Left: {Lives}");
        }
        else
        {
            Console.WriteLine($"{Name} already used all lives!");
        }
    }
}

Using it in code:

var barsik = new Cat("Whiskers");
barsik.Speak();      // Whiskers says: Meow! I have 9 lives.
barsik.LoseLife();   // Whiskers lost one life. Left: 8

Adding new classes: expanding the "zoo"

You already know how to make derived classes. Let's add, for example, a parrot:

public class Parrot : Animal
{
    public Parrot(string name) : base(name) { }

    public override void Speak()
    {
        Console.WriteLine($"{Name} says: Hi, human!");
    }

    public void Repeat(string phrase)
    {
        Console.WriteLine($"{Name} repeats: {phrase}");
    }
}

Now you can easily expand the system without touching old code:

var keshka = new Parrot("Polly");
keshka.Speak();                 // Polly says: Hi, human!
keshka.Repeat("Learn, student!"); // Polly repeats: Learn, student!

4. Comparing animal behavior

Type Speak() method Own field Extra behavior
Animal Yes (virtual) Name —
Cat Yes (override) Lives LoseLife()
Dog Yes (override) — —
Parrot Yes (override) — Repeat(string)

What the class hierarchy looks like in memory (block diagram)

Animal (Name)
 ├── Cat (Lives)
 ├── Dog
 └── Parrot (Repeat)

5. Practice in our app

Let's tie the hierarchy idea to an app—for example, we have tasks of different types:

  1. Task (base class): Any task—it has a title and a completion status.
  2. WorkTask (work): In addition, has a deadline.
  3. HomeTask (home): Can have a priority ("Very important", "So-so").

Let's start with the base class:

public class Task
{
    public string Title { get; set; }
    public bool IsCompleted { get; private set; }

    public Task(string title)
    {
        Title = title;
    }

    public virtual void Complete()
    {
        IsCompleted = true;
        Console.WriteLine($"Task \"{Title}\" completed!");
    }
}

Now let's add a work task:

public class WorkTask : Task
{
    public DateTime Deadline { get; set; }

    public WorkTask(string title, DateTime deadline)
        : base(title)
    {
        Deadline = deadline;
    }

    public override void Complete()
    {
        base.Complete();
        Console.WriteLine($"Deadline: {Deadline:d}");
    }
}

And a home task:

public class HomeTask : Task
{
    public string Priority { get; set; }

    public HomeTask(string title, string priority)
        : base(title)
    {
        Priority = priority;
    }

    // You don't have to override Complete if the base class behavior is enough
}

Let's make a list of tasks in the program:

List<Task> tasks = new List<Task>
{
    new WorkTask("Send report", DateTime.Today.AddDays(2)),
    new HomeTask("Wash dishes", "Very important"),
    new Task("Read inheritance lecture")
};

foreach (Task task in tasks)
{
    Console.WriteLine($"Task: {task.Title}");
    task.Complete();
}

Expected output:

Task: Send report
Task "Send report" completed!
Deadline: 13.07.2025
Task: Wash dishes
Task "Wash dishes" completed!
Task: Read inheritance lecture
Task "Read inheritance lecture" completed!

See how handy it is: all tasks are stored together, we process them the same way, and the specifics show up where needed.

6. Common mistakes when using inheritance

Mistake #1: trying to override a method that's not marked virtual.
If a method in the base class isn't marked virtual, you can't override it in derived classes. As a result, all the flexibility of polymorphism is lost, and the hierarchy becomes useless.

Mistake #2: inheritance without logical connection between entities.
Don't use inheritance if the objects aren't logically related. For example, Circle really is a Figure, but Horse as a Vehicle—that's a stretch. Exception—specific contexts (like a medieval game), where that connection might make sense.

Mistake #3: overly deep hierarchies.
When your class structure goes deep (5–6 or more levels), the code gets hard to read, maintain, and test. That's a sign you should consider composition as an alternative to inheritance.

Mistake #4: forgot to call the base constructor.
When adding new properties in a derived class, it's easy to forget to explicitly call base(...) in the constructor. This can lead to incomplete or incorrect initialization of the base part of the object and to sneaky bugs.

2
Task
C# SELF, level 20, lesson 4
Locked
Creating a Base and Derived Class
Creating a Base and Derived Class
2
Task
C# SELF, level 20, lesson 4
Locked
Extending a class hierarchy
Extending a class hierarchy
1
Survey/quiz
The Concept of Inheritance, level 20, lesson 4
Unavailable
The Concept of Inheritance
Inheritance and Class Hierarchy
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION