Skip to main content
ANVISoftware Solutions
Lesson 26 of 62Intermediate20 min

Inheritance

By the end of this lesson

Share behaviour through a base class, and recognise when composition is the better option.

Inheritance lets one class build on another. The new class starts with everything the original had — its properties, its methods — and adds or changes what it needs. The original is the base class; the new one is the derived class.

The relationship it models is "is a kind of". A manager is a kind of employee. A savings account is a kind of account. When that sentence is true of the real thing you are modelling, inheritance describes it accurately.

It is worth saying plainly at the start: inheritance is frequently reached for as a way to avoid retyping code, and that is the use it handles worst. This lesson teaches the mechanism and then makes the case for a different default.

A base class and a derived class
C#
public class Employee
{
    public string Name { get; }
    public string Role { get; }

    public Employee(string name, string role)
    {
        Name = name;
        Role = role;
    }

    public string Describe()
    {
        return $"{Name} ({Role})";
    }
}

public class Manager : Employee
{
    public int DirectReports { get; }

    public Manager(string name, int directReports)
        : base(name, "Manager")
    {
        DirectReports = directReports;
    }
}

Manager priya = new Manager("Priya", 6);

Console.WriteLine(priya.Describe());        // Priya (Manager)
Console.WriteLine(priya.DirectReports);     // 6

Employee asEmployee = priya;                // a Manager is usable anywhere an Employee is
  • public class Manager : Employee declares Employee as the base class. A class can have only one base class in C#.
  • Manager does not redeclare Name, Role or Describe. It has them because it inherits them.
  • : base(name, "Manager") calls the base constructor. It runs before the Manager constructor body, because the base part of the object has to be set up first.
  • The last line is the property that makes inheritance useful: a Manager can be treated as an Employee. Code written against Employee works with every kind of employee you ever add.
How a harmless-looking base class breaks its subclass
C#
public class OrderBook
{
    protected readonly List<Order> Stored = new List<Order>();

    public virtual void Add(Order order)
    {
        Stored.Add(order);
    }

    public virtual void AddMany(List<Order> orders)
    {
        foreach (Order order in orders)
        {
            Add(order);            // an internal detail: AddMany routes through Add
        }
    }
}

public class CountingOrderBook : OrderBook
{
    public int AddedCount { get; private set; }

    public override void Add(Order order)
    {
        AddedCount++;
        base.Add(order);
    }

    public override void AddMany(List<Order> orders)
    {
        AddedCount += orders.Count;
        base.AddMany(orders);      // which calls Add, which counts again
    }
}
  • virtual on a base method means a derived class is allowed to replace it. override on the derived method is how it does so. The next lesson covers both properly.
  • Read CountingOrderBook on its own and it looks correct. Both methods count what they add and then hand off to the base.
  • Add three orders through AddMany and AddedCount becomes six. AddMany counted three, then base.AddMany called the overridden Add three more times.
  • The author of CountingOrderBook could not have known this without reading the base class's source. Nothing in the public signature of AddMany says "this calls Add internally".
  • Fixing it means removing the override of AddMany — which is only correct while the base keeps routing through Add. A later refactor of the base class, changing nothing public, breaks the subclass again.

That is the fragile base class problem. A derived class does not depend only on the base class's public surface; it depends on how the base class is built inside — which methods call which, in what order, and what state they expect. Those internal details are not documented and are not part of any contract, so they change freely. When they change, subclasses break, and the break is often silent.

The effect compounds. Once a class has subclasses, its internal call structure has quietly become part of its interface, and the author can no longer safely refactor it. Inheritance is the tightest coupling C# offers between two classes, and it is the only one you cannot loosen later without rewriting both sides.

The same feature by composition — holding an object instead of extending it
C#
public class CountingOrderBook
{
    private readonly OrderBook _inner;

    public int AddedCount { get; private set; }

    public CountingOrderBook(OrderBook inner)
    {
        _inner = inner;
    }

    public void Add(Order order)
    {
        AddedCount++;
        _inner.Add(order);
    }

    public void AddMany(List<Order> orders)
    {
        AddedCount += orders.Count;
        _inner.AddMany(orders);
    }
}
  • No base class. CountingOrderBook holds an OrderBook in a private field and forwards work to it. This is composition: "has a" rather than "is a".
  • The double-counting cannot happen. _inner.AddMany may call Add internally, but that is OrderBook's own Add, not this class's, so it is invisible here.
  • The only thing this class depends on is OrderBook's public methods. OrderBook can be rewritten inside however its author likes.
  • The cost is visible in the code: two forwarding methods that inheritance would have supplied free. That is the trade you are making — a small amount of typing in exchange for not depending on someone else's internals.

Summary

  • Inheritance gives a derived class the base class's members and lets it stand in for the base type
  • : base(...) calls the base constructor, which runs before the derived constructor body
  • A derived class depends on the base class's internal behaviour, not only its public surface — this is the fragile base class problem
  • Composition means holding an object and forwarding to it, which depends on public methods only
  • Prefer composition by default; use inheritance for genuine specialisation where you own both sides

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Think about it

Think about it

A team has Invoice and CreditNote classes. They share a reference, a customer, a date, a list of lines and a total — about eighty per cent of their contents. Someone proposes a BillingDocument base class to remove the duplication.

What would you want to know before agreeing, and what would change your answer?

Show solution

The first question is whether any code will ever hold a BillingDocument without caring which it is. If nothing does, the base class buys no substitutability and you are paying inheritance's coupling cost purely to avoid retyping field declarations.

The second is whether the two really behave alike. A credit note reduces what is owed and an invoice increases it, so the totals have opposite signs and the approval rules differ. Shared field names can hide genuinely different behaviour, and behaviour is what a base class ties together.

The third is how they will evolve. If invoices are about to gain payment terms, instalments and reminders that credit notes will never have, the shared base will become a place to put things that only apply to half its subclasses — the clearest sign the abstraction is wrong.

A defensible middle answer: extract the shared data into a class that both hold, such as a DocumentHeader, and let both types own one. No inheritance, no duplication, and the two can evolve apart. If it later turns out that plenty of code genuinely wants to treat them interchangeably, an interface gives you that without the coupling.

Challenge

Challenge

Run the fragile base class example yourself: create three orders, pass them to AddMany on a CountingOrderBook, and print AddedCount.

Then fix it in two different ways — once by changing only CountingOrderBook, and once by converting to composition. Write down which fix you would still trust after someone else refactors OrderBook.

Show solution

The minimal fix is to delete the AddMany override entirely. Counting then happens only in Add, which base.AddMany calls for each order, giving three.

That fix is correct today and depends entirely on an undocumented detail: that AddMany goes through Add. Change the base class to call Stored.AddRange directly — a reasonable performance improvement that alters nothing public — and the count silently becomes zero.

The composition version does not have that failure mode, because it never relies on what OrderBook does internally. It is the fix that survives someone else's refactoring, which is the property worth paying two forwarding methods for.

Saved in this browser only.