C# Fundamentals: From Your First Program to Async and JSON · C# Basics: Types, Collections, Classes and Pattern Matching

Interfaces, Inheritance and Polymorphism

Define contracts with interfaces, share behavior with abstract base classes, override members, and prefer composition and dependency injection over deep inheritance.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A notification system. A INotifier interface defines the contract, different notifiers (email, SMS, console) implement it, and a Reminder service uses any notifier through the interface, so new channels can be added without touching the service.

The idea

  • Interface (interface INotifier { void Send(string to, string message); }): a contract with no state. A class can implement many interfaces. Name them with an I prefix.
  • Abstract class: a partial implementation that can't be instantiated, with abstract members subclasses must implement and virtual members they may override. A class can inherit from only one base class.
  • override replaces a virtual member; sealed stops further inheritance.
  • Polymorphism: code that depends on an interface works with any implementation. This is what makes testing easy: pass a fake in tests.
  • Dependency injection (DI): a class receives its dependencies (as constructor parameters) instead of creating them. ASP.NET Core has DI built in.
  • Default interface members let an interface provide an implementation (useful for evolving interfaces).

Guideline: program to interfaces, inherit rarely, compose often. Deep inheritance trees are hard to change; small interfaces and composition are not.

The code

An INotifier interface with several implementations
var log = new List<string>();

INotifier[] channels =
[
    new ConsoleNotifier(),
    new EmailNotifier("[email protected]"),
    new PrefixedNotifier(new ConsoleNotifier(), "[URGENT]"),   // composition: wraps another notifier
];

foreach (var channel in channels)
{
    var reminder = new ReminderService(channel);
    reminder.Remind("ada", "Stand-up in 5 minutes");
}

// A fake notifier makes the service easy to test
var fake = new FakeNotifier(log);
new ReminderService(fake).Remind("grace", "Deploy at 17:00");
Console.WriteLine("captured: " + string.Join(" | ", log));

// Abstract base class with a shared template method
Shape[] shapes = [new Rect(3, 4), new Circle(1.5)];
foreach (var shape in shapes) Console.WriteLine(shape.Describe());

interface INotifier
{
    void Send(string to, string message);
    string Channel => GetType().Name.Replace("Notifier", "");    // default interface member
}

class ConsoleNotifier : INotifier
{
    public void Send(string to, string message) => Console.WriteLine($"[console] {to}: {message}");
}

class EmailNotifier(string from) : INotifier            // primary constructor (C# 12)
{
    public void Send(string to, string message) => Console.WriteLine($"[email from {from}] to {to}@example.com: {message}");
}

class PrefixedNotifier(INotifier inner, string prefix) : INotifier
{
    public void Send(string to, string message) => inner.Send(to, $"{prefix} {message}");
}

class FakeNotifier(List<string> log) : INotifier
{
    public void Send(string to, string message) => log.Add($"{to}:{message}");
}

// The service depends on the interface, not on a concrete class
class ReminderService(INotifier notifier)
{
    public void Remind(string user, string text) => notifier.Send(user, text);
}

abstract class Shape
{
    public abstract double Area();
    public virtual string Name => GetType().Name;
    public string Describe() => $"{Name}: area {Area():F2}";     // uses the subclass's Area()
}

class Rect(double width, double height) : Shape
{
    public override double Area() => width * height;
}

class Circle(double radius) : Shape
{
    public override double Area() => Math.PI * radius * radius;
    public override string Name => "Round shape";
}

Output

[console] ada: Stand-up in 5 minutes
[email from [email protected]] to [email protected]: Stand-up in 5 minutes
[console] ada: [URGENT] Stand-up in 5 minutes
captured: grace:Deploy at 17:00
Rect: area 12.00
Round shape: area 7.07

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Large "god" interfaces. Split them so implementers aren't forced to implement unused members.
  • Creating dependencies inside a class (new EmailNotifier() in the constructor). Inject them instead, so you can substitute fakes.
  • Inheriting to share code. If the relationship isn't "is a", use composition (hold a field or constructor parameter).
  • Forgetting virtual/override. Without them, a subclass member hides the base member rather than replacing it.
  • Casting from interface to concrete type. If you need to, the abstraction is probably wrong.

Key points

  • Interfaces are contracts; classes implement several. Depend on interfaces, not concrete classes.
  • Abstract classes share implementation; virtual and override enable polymorphism.
  • Constructor injection supplies dependencies and makes code testable.
  • Prefer composition and small interfaces over deep inheritance.

Further reading: Interfaces.

Practice questions

Why does `ReminderService` take an `INotifier` in its constructor rather than creating an `EmailNotifier`?

Answer

Depending on the interface means any implementation can be supplied: a different channel in production or a fake in tests. Creating the concrete class inside would tie the service to it and make it hard to test or change.

What is the difference between an interface and an abstract class?

Answer

An interface is a pure contract (with optional default members) and a class can implement many. An abstract class can hold state and shared implementation but a class can inherit from only one.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?