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.
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 anIprefix. - Abstract class: a partial implementation that can't be instantiated, with
abstractmembers subclasses must implement andvirtualmembers they may override. A class can inherit from only one base class. overridereplaces a virtual member;sealedstops 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
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;
virtualandoverrideenable 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.