MOO WebTech
OOPtheory-practiceintermediate

L12 — OOP pt.3: Inheritance & Interfaces

The last OOP pillar: how classes can reuse and extend each other. We learn inheritance, virtual/override, abstract classes, and interfaces — and when to use each.

80 min24.04.2026L12

🎯Learning Objectives

  • Create a derived class that inherits fields, properties, and methods from a base class
  • Use protected to expose internals to subclasses without exposing them to the world
  • Call a base constructor with base(...)
  • Override a base method using virtual / override
  • Define an interface and implement it in a class
  • Explain when to reach for inheritance vs an interface

📖Theory

1. Why Inheritance?

Imagine you're adding three new classes to your school app: Student, Teacher, Librarian. They all have Name, Email, and a Greet() method. Writing the same three properties and method three times is copy-paste programming — the worst kind of code to maintain.

Inheritance lets one class (the derived class) take everything another class (the base class) has, and then add or change pieces on top. You write the common stuff once, in the base.

C#
class Person
{
    public string Name  { get; set; }
    public string Email { get; set; }

    public void Greet() =>
        Console.WriteLine($"Hi, I'm {Name}.");
}

class Student : Person       // Student "is-a" Person
{
    public double GPA { get; set; }
}

class Teacher : Person       // Teacher "is-a" Person
{
    public string Subject { get; set; }
}

The colon : Person means inherits from Person. Student and Teacher automatically get Name, Email, and Greet() — for free.

C#
Student s = new Student { Name = "Alice", Email = "a@x.com", GPA = 3.8 };
s.Greet();                 // Hi, I'm Alice. ← method came from Person
Console.WriteLine(s.GPA);  // 3.8           ← added by Student

The "is-a" test. Inheritance only makes sense when the sentence "a Student is a Person" sounds right. If you find yourself saying "a Car has an Engine" — that's composition, not inheritance. Use a field of type Engine, not Car : Engine.

2. protected — Access for Subclasses Only

In L11 we made fields private. Private means only this class. But a subclass often needs to reach into its parent's state. protected is the middle ground: this class and any class that inherits from it.

C#
class Person
{
    protected string _internalId;   // subclasses see this; outside code doesn't

    public Person(string id) => _internalId = id;
}

class Student : Person
{
    public Student(string id) : base(id) { }

    public void DebugPrint() =>
        Console.WriteLine(_internalId);   // ✅ ok — protected is visible here
}

Outside code still can't touch _internalId. Only Person and its descendants can.

3. Calling the Base Constructor — base(...)

When a derived class has a constructor, it must call one of the base constructors so the base part of the object gets initialised. You do that with base(...):

C#
class Person
{
    public string Name  { get; }
    public string Email { get; }

    public Person(string name, string email)
    {
        Name  = name;
        Email = email;
    }
}

class Student : Person
{
    public double GPA { get; set; }

    // Call Person's constructor first, then run the Student-specific part
    public Student(string name, string email, double gpa) : base(name, email)
    {
        GPA = gpa;
    }
}

The order is always: base constructor → derived constructor body. C# enforces it automatically.

4. Overriding Methods — virtual / override

Sometimes a derived class wants to change what a base method does. The base marks the method virtual (meaning "you may override me"), and the derived class writes its own version marked override.

C#
class Person
{
    public string Name { get; set; }

    public virtual void Greet() =>
        Console.WriteLine($"Hi, I'm {Name}.");
}

class Teacher : Person
{
    public string Subject { get; set; }

    public override void Greet() =>
        Console.WriteLine($"Hi, I'm {Name}, I teach {Subject}.");
}

Calling Greet() on a Teacher runs the Teacher version, not the Person one:

C#
Teacher t = new Teacher { Name = "Mr. Smith", Subject = "Math" };
t.Greet();   // Hi, I'm Mr. Smith, I teach Math.

You can still reach the base version from inside the override with base.Greet():

C#
public override void Greet()
{
    base.Greet();                       // Hi, I'm Mr. Smith.
    Console.WriteLine($"I teach {Subject}.");
}

5. Polymorphism — One Variable, Many Shapes

Because a Teacher is also a Person, you can store a Teacher in a Person-typed variable. And when you call Greet() on that variable, C# picks the right version at runtime based on the actual object — not the variable's declared type. This is called polymorphism.

C#
Person[] people = new Person[]
{
    new Student { Name = "Alice" },
    new Teacher { Name = "Mr. Smith", Subject = "Math" },
    new Person  { Name = "Bob" }
};

foreach (Person p in people)
    p.Greet();
// Hi, I'm Alice.
// Hi, I'm Mr. Smith, I teach Math.
// Hi, I'm Bob.

One loop, one line, three different behaviours — chosen by the object itself. This is the feature that makes OOP worth using.

6. Abstract Classes — Blueprints That Can't Be Built Alone

Sometimes a base class is too generic to exist on its own. "Shape" is a shape — but you never hold a plain "shape" in your hand; you hold a circle or a square. An abstract class is a base you can't directly instantiate. It can also declare abstract methods — methods with no body that every subclass must fill in.

C#
abstract class Shape
{
    public abstract double Area();   // no body — subclasses must override

    public void Print() =>
        Console.WriteLine($"Area = {Area()}");
}

class Circle : Shape
{
    public double Radius { get; set; }
    public override double Area() => Math.PI * Radius * Radius;
}

class Square : Shape
{
    public double Side { get; set; }
    public override double Area() => Side * Side;
}

// Shape s = new Shape();     // ❌ compile error — abstract, can't be created
Shape c = new Circle { Radius = 5 };
c.Print();                     // Area = 78.53...

7. Interfaces — A Contract Without Any Code

An interface is a pure contract: a list of methods/properties a class promises to provide, with no implementation. A class can inherit from only one base class, but it can implement many interfaces.

C#
interface IPrintable
{
    void Print();
}

interface ISaveable
{
    void Save();
}

class Report : IPrintable, ISaveable
{
    public string Title { get; set; }

    public void Print() => Console.WriteLine($"Printing: {Title}");
    public void Save()  => Console.WriteLine($"Saving: {Title}");
}

Naming convention. Interfaces in C# start with a capital I: IPrintable, IDisposable, IEnumerable. This is just a convention, but the whole ecosystem follows it — stick to it.

You can also use an interface as a variable type. That's powerful: the code doesn't care which class it got, only that it honours the contract.

C#
IPrintable[] printables = new IPrintable[] { new Report { Title = "Q1" } };
foreach (IPrintable p in printables)
    p.Print();

8. Inheritance vs Interface — Which One?

SituationUse
A genuine "is-a" relationship and you want to share implementationInheritance (base class)
A capability that several unrelated classes can have (printable, comparable, drawable)Interface
A base with some shared code and some methods subclasses must fill inAbstract class
You need to combine behaviour from multiple sourcesInterfaces (you can implement many, inherit one)

Default in modern C#: prefer interfaces when you're designing boundaries between parts of your app. Reach for inheritance when you truly share state and behaviour and the "is-a" sentence fits.

💻Code Examples

Example A — Employee hierarchy with overriding

C#
class Employee
{
    public string Name  { get; set; }
    public decimal BaseSalary { get; set; }

    public virtual decimal MonthlyPay() => BaseSalary;

    public void Print() =>
        Console.WriteLine($"{Name}: {MonthlyPay():C}");
}

class Manager : Employee
{
    public decimal Bonus { get; set; }

    public override decimal MonthlyPay() => BaseSalary + Bonus;
}

class Intern : Employee
{
    public override decimal MonthlyPay() => BaseSalary / 2m;   // interns earn half
}

Employee[] team = new Employee[]
{
    new Manager { Name = "Alice", BaseSalary = 5000m, Bonus = 1500m },
    new Intern  { Name = "Bob",   BaseSalary = 2000m },
    new Employee{ Name = "Carol", BaseSalary = 3000m }
};

foreach (Employee e in team) e.Print();
// Alice: $6,500.00
// Bob:   $1,000.00
// Carol: $3,000.00

Example B — Interface-based comparison

C#
interface IComparableSize
{
    double SizeMetric();
}

class Book : IComparableSize
{
    public string Title { get; set; }
    public int Pages    { get; set; }

    public double SizeMetric() => Pages;
}

class Movie : IComparableSize
{
    public string Title     { get; set; }
    public int MinutesLong  { get; set; }

    public double SizeMetric() => MinutesLong;
}

IComparableSize a = new Book  { Title = "1984",   Pages = 328 };
IComparableSize b = new Movie { Title = "Matrix", MinutesLong = 136 };
Console.WriteLine(a.SizeMetric() > b.SizeMetric() ? "a is bigger" : "b is bigger");

Notice how the loop doesn't know or care about Book vs Movie — it only speaks the interface.

✏️Practice Tasks

Task 1Animal hierarchy
EASY — IN CLASS

Create an Animal base class with property Name (string) and a virtual method Speak() that prints "some sound".

Create three derived classes:

  • Dog : Animal — overrides Speak() to print "{Name} says: Woof!"
  • Cat : Animal — overrides Speak() to print "{Name} says: Meow!"
  • Cow : Animal — overrides Speak() to print "{Name} says: Moo!"

In the main program, create an Animal[] containing a Dog, Cat, and Cow. Loop through and call Speak() on each.

💡 Hint
Don't forget the virtual on the base method and override on each derived one — otherwise the loop will call the base version every time. If that happens, check both keywords.
Task 2Shapes with abstract base
MEDIUM — HOMEWORK

Build the Shape hierarchy from section 6, but add a third subclass Rectangle(Width, Height). Then:

  • Create an array Shape[] with a Circle (radius 3), a Square (side 4), and a Rectangle (5 × 2)
  • Loop through the array, call Print() on each, and also print the total area of all shapes combined
  • Try writing Shape s = new Shape(); and explain in a comment why the compiler refuses
💡 Hint
Keep a running sum in a double total = 0; variable before the loop, then do total += shape.Area(); inside. After the loop, print the total. The abstract class can't be instantiated because at least one of its methods has no body — there's nothing to run.
Task 3ILogger interface with two implementations
HARD — HOMEWORK

Define an interface:

C#
interface ILogger
{
    void Log(string message);
}

Create two classes that implement it:

  • ConsoleLogger — prints "[CONSOLE] {message}" with a timestamp
  • FileLoggerpretends to write to a file by printing "[FILE:log.txt] {message}" (don't actually write to disk for this task)

Create a method RunApp(ILogger logger) that calls logger.Log("App started"), logger.Log("Doing work"), logger.Log("App finished"). Call RunApp twice — once with each logger. Observe that RunApp doesn't change, only the logger you pass in does.

💡 Hint
This task is the whole point of interfaces: your code (RunApp) depends on the contract, not the concrete class. Swapping implementations requires zero changes to the code that uses them. This pattern — "program to an interface" — is one of the most useful ideas in OOP, and you'll see it again in MAUI with services.

⚠️Common Mistakes

Forgetting virtual on the base method

If the base method isn't virtual, the derived override won't compile. And if you use the new keyword instead of override to "hide" the method, polymorphism breaks — calling through the base-typed variable will run the base version. Use virtual/override, not new, unless you know exactly why.

Trying to inherit from two classes

C# allows only single inheritance of classes: class A : B, C is illegal. If you need to combine behaviours from multiple sources, use interfaces — you can implement as many as you want.

Confusing abstract and interface

An abstract class can contain real code and state; an interface (until recently) cannot. If you find yourself wanting some shared fields, it has to be an abstract class. If you only want to list required methods, it's an interface.

Making every class inherit from something

Inheritance is overused. Before adding : SomeBase, say the "is-a" sentence out loud. "An Employee is a Person" — yes, ok. "A Logger is a Writer" — that's shakier; maybe just compose.

Upcasting without downcasting carefully

C#
Person p = new Student();       // upcast — always safe
Student s = (Student)p;          // downcast — crashes at runtime if p is not really a Student

Use the is keyword to check first: if (p is Student s) { ... }.

🎓Instructor Notes

⚡ How to run this lesson (~80 min)

  • [10 min] Motivation via copy-paste pain. Put three classes with duplicated Name/Email/Greet on the projector. Ask: "What's wrong with this code?" Let them say it. Introduce : Person.
  • [15 min] Live code: base → derived → override. Build Person → Student → Teacher. Use the virtual/override pair. Then store a Teacher in a Person variable and call Greet(). This is the "aha" moment — don't rush past it.
  • [15 min] Abstract class or interface? Show the Shape example. Show the ILogger interface example. Spend time on the table in section 8. This is conceptual — many students conflate abstract and interface the first week.
  • [5 min] Protected + base(...) walkthrough.
  • [30 min] Task 1 in class. Watch for: missing virtual, missing override, trying to instantiate the abstract class in Task 2 — have the expected compile errors on the projector as teaching moments.
  • [5 min] Wrap up + assign homework. Preview L13: leaving the console and entering .NET MAUI. Assign Tasks 2 and 3.

💬 Discussion questions

  • "Give an example from real life where inheritance feels natural. Now one where it would feel wrong."
  • "Why can't an abstract class be created with new?"
  • "In Task 3, what would change inside RunApp if we added a third logger (e.g., DatabaseLogger)?"