1. Introduction
Let's take a bird's-eye view of abstraction: imagine a big office where different employees do different tasks. They might all have a common job title — "Employee", but each has their own special assignment. But if you're the boss and you want every employee to be able to "DoWork", you really don't care how exactly it's done: the programmer writes code, the accountant types up reports. This idea of "a common contract for everyone via abstraction, details — for the specialists" is at the heart of building class hierarchies the right way.
What does this look like in code?
It all starts with a base abstract class. It describes the most important thing that all its descendants must be able to do.
public abstract class Employee
{
public string Name { get; }
public Employee(string name)
{
Name = name;
}
// Abstract method — contract for all employees
public abstract void DoWork();
}
Next come the derived classes (descendants) — programmer and accountant:
public class Programmer : Employee
{
public Programmer(string name) : base(name) { }
public override void DoWork()
{
Console.WriteLine($"{Name} writes code");
}
}
public class Accountant : Employee
{
public Accountant(string name) : base(name) { }
public override void DoWork()
{
Console.WriteLine($"{Name} counts money");
}
}
Now check this out: we can make a list of employees — accountants, programmers, whoever else we want (abstraction totally doesn't mind this at all!).
Employee[] office = new Employee[]
{
new Programmer("Vasya"),
new Accountant("Tanya")
};
foreach (Employee emp in office)
{
emp.DoWork(); // Everyone does their own job
}
Vasya writes code
Tanya counts money
Notice how everything got nice, universal, and most importantly, scalable. You can add a new class, like Manager, and you don't have to change any of the code you already wrote!
2. How to Build Hierarchies Based on Abstractions
Since pretty much every OOP textbook can't live without an animal example, let's keep the tradition going.
Abstract class — playing the "Big Dictator" role
public abstract class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// All animals must be able to make a sound
public abstract void MakeSound();
// But not everyone has to fly, so:
public virtual void Fly()
{
Console.WriteLine("I can't fly.");
}
}
Child classes: must make a sound, can override other methods too
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name}: Meow!");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name}: Woof!");
}
}
public class Eagle : Animal
{
public Eagle(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name}: Screech!");
}
public override void Fly()
{
Console.WriteLine($"{Name} flies in the sky!");
}
}
Now let's try making a collection of different animals:
Animal[] zoo = new Animal[]
{
new Cat("Barsik"),
new Dog("Sharik"),
new Eagle("Orel")
};
foreach (Animal animal in zoo)
{
animal.MakeSound();
animal.Fly();
}
Barsik: Meow!
I can't fly.
Sharik: Woof!
I can't fly.
Orel: Screech!
Orel flies in the sky!
Notice how easy it is now to use any descendant classes: we don't care what exact object is in the collection. Abstraction takes care of the "contract," and the implementation details handle the specific behavior.
3. Abstraction on Multiple Levels
Abstraction isn't just about splitting into a base class and its direct descendants. In complex systems, it's often done in several layers, where one abstract class is built on top of another. It's like a layer cake (or an onion, as Shrek would say): each layer hides extra details, leaving only what you need.
Example: Vehicle Hierarchy
. Vehicle (abstract)
/ | \
Car Airplane Boat
ElectricCar JetAirplane SailBoat
Here, Vehicle sets the general principles (like the Move() method), but doesn't know how exactly a car or a plane will move. That's up to the derived classes. More specific classes like ElectricCar or JetAirplane can add their own details, expanding the functionality.
Block diagram of the hierarchy:
. +------------------+
| Vehicle | <--- abstract class
+------------------+
/ \
+-------+ +-------+
| Car | | Boat | <--- intermediate abstractions or concrete classes
+-------+ +-------+
Code: base abstract class
public abstract class Vehicle
{
public string Model { get; }
public Vehicle(string model)
{
Model = model;
}
// Abstract method
public abstract void Move();
}
Intermediate abstract class
Sometimes you need your own abstractions at the intermediate level too!
public abstract class Car : Vehicle
{
public Car(string model) : base(model) { }
public override void Move()
{
Console.WriteLine($"{Model} drives on the road.");
}
// Abstract method — not every car is electric:
public abstract void RefuelOrCharge();
}
Concrete implementations
public class ElectricCar : Car
{
public ElectricCar(string model) : base(model) { }
public override void RefuelOrCharge()
{
Console.WriteLine($"{Model} charges with electricity.");
}
}
public class GasolineCar : Car
{
public GasolineCar(string model) : base(model) { }
public override void RefuelOrCharge()
{
Console.WriteLine($"{Model} refuels with gasoline.");
}
}
Result: multi-level abstraction makes it easier to add new classes and features.
Visualization: example class hierarchy
| Class | Type | Abstract | Parent | Special Behavior |
|---|---|---|---|---|
| Vehicle | Base | Yes | - | Abstract method Move() |
| Car | Intermediate | Yes | Vehicle | Abstract RefuelOrCharge() |
| ElectricCar | Final | No | Car | Implements RefuelOrCharge() |
| GasolineCar | Final | No | Car | Implements RefuelOrCharge() |
| Boat | Final | No | Vehicle | Implements Move() |
4. Using Abstraction for Scalable Apps
Practical benefits:
- Flexibility and code scalability: You can add a new class (like a new type of animal, vehicle, or employee) without changing already working code. Your loops/methods will work with the new object automatically — as long as it implements the methods defined in the abstraction.
- Minimizing code duplication: Common properties and methods (like name, base logic) are defined once in the abstract class, and descendants get them automatically.
- Common approach in big frameworks and systems: For example, in ASP.NET MVC there are abstract base controllers (ControllerBase), and in WinForms — the abstract class Control for all UI elements. This lets you extend the framework without breaking the stability of already written parts.
Where is this useful?
- Any big systems: banking apps (employees, operations and transactions), games (game entities, characters), business apps (catalogs, products, users), any hierarchical and structured data.
- Technical interviews: being able to build classes based on abstractions and explain hierarchies is a common interview question.
- Program architecture: abstraction lets you build the "skeleton" of the architecture even before writing the logic — handy for team development, TDD (test-driven development), and just for maintainability.
5. Typical Mistakes and Pitfalls When Building Hierarchies
Mistake #1: forgot to implement an abstract method.
The compiler won't let you create an instance of a derived class if it doesn't implement all the abstract members of the base class. This is a must-follow rule — like an unwritten law: if you want to be concrete, implement everything.
Mistake #2: trying to inherit from multiple abstract classes.
In C#, you can't inherit from more than one class at the same time, even if they're all abstract. It's a language limitation. If you need to "inherit" behavior from different sources — use interfaces. They're like abstract classes, just lighter and with no implementation.
Mistake #3: not understanding the point of intermediate abstract classes.
These classes are often used to group common logic and protect against creating "under-defined" objects. For example, the Car class can be abstract so no one creates just a "car" without specifying what kind: gasoline, diesel, or electric.
GO TO FULL VERSION