Abstract Classes vs Interfaces: Report Cards and Senders – C# | Lesson 41
Introduction
Is there such a thing as a plain "person" in the school? No: everyone is a student, a
teacher, or a guardian. Yet the program from Lesson 40 still allows new Person(...).
An abstract class fixes this: it can only be inherited from, never created directly. An interface goes further: it describes what a class can do, without saying how. Both are tools for abstraction, the fourth OOP pillar: code that depends on what something does, not on how it does it.
By the end of this lesson, you will be able to:
- Write an abstract class with abstract and normal members.
- Write an interface and implement it in several classes.
- Use a default interface method.
- Give one class a base class and several interfaces.
- Choose between an abstract class and an interface, and explain why.
Why this matters
"What is the difference between an abstract class and an interface?" is one of the most asked C# interview questions, at every level. Freshers recite definitions; strong candidates explain when they would use each, with an example.
In the Web API, interfaces are everywhere. Controllers depend on IStudentService, not on
a concrete class, so a test can swap in a fake one. This lesson builds that habit on small,
clear examples first.
The concept
Example: an abstract class
public abstract class ReportCard
{
public string FormatLine(string subject, decimal? marks, decimal maxMarks) { /* shared code */ }
protected abstract string FormatScore(decimal marks, decimal maxMarks);
}
| Part | Meaning |
|---|---|
abstract class | Cannot be created with new; only inherited from |
| Normal members | Shared code that every derived class gets |
abstract member | No body: every derived class must override it |
Think of a school's printed report card template. The header, layout, and signature box are already printed. The space for scores is left blank, and each stage of school fills it in its own way.
Example: an interface
public interface INotificationSender
{
string Channel { get; }
bool CanReach(Guardian guardian);
string Send(Guardian guardian, string message);
}
An interface is a contract: a list of members that a class promises to provide. It has
no fields, no constructors, and usually no code. Interface names start with I by
convention.
Think of a job description for "school messenger". It lists what the person must be able to do: say how they deliver, check whether they can reach someone, and deliver a message. It says nothing about whether they use email or a phone.
Example: abstract class or interface?
| Abstract class | Interface | |
|---|---|---|
| Can contain code | Yes: fields, constructors, normal methods | Only default methods, no instance fields |
| A class can have | One base class | Many interfaces |
| Relationship | "is a" (a primary report card is a report card) | "can do" (an email sender can send notifications) |
| Members | Any access level, including protected | Public by default |
| Best for | Related classes that share code | A capability that unrelated classes can offer |
Use an abstract class when related classes share real code. Use an interface when you only need a promise of behaviour, or when unrelated classes must offer the same capability.
Example: default interface methods
Since C# 8, an interface may give a member a default body:
string Describe() => $"Sends by {Channel}";. Implementing classes get it for free, and
may write their own.
There is a catch: the default is reached only through the interface type. On an
EmailSender variable, email.Describe() fails with error CS1061: 'EmailSender' does not contain a definition for 'Describe'. Through an INotificationSender variable it works.
Example: interfaces you already use
.NET itself is built on interfaces. Some you have used without seeing them:
| Interface | Gives a class the ability to | You used it when |
|---|---|---|
IEnumerable<T> | Be walked through with foreach | Looping over arrays (Lesson 32) |
IComparable<T> | Be sorted | Sorting comes in Module 8 |
IDisposable | Release files and connections with using | Database connections, Module 9 |
IEquatable<T> | Compare by value | Records, Lesson 43 |
A foreach loop does not care whether it walks an array, a list, or a database result. It
works with anything that implements IEnumerable<T>.
Example: adding a member later
Contracts are hard to change once others rely on them. The two tools behave differently when you add something new:
| Add a new member to | Existing implementations |
|---|---|
| An interface, without a body | All stop compiling until they implement it |
| An interface, with a default body | Keep working; they get the default |
| An abstract class, as a normal or virtual method | Keep working; they inherit it |
| An abstract class, as an abstract member | All stop compiling until they override it |
This is why public libraries think carefully before adding interface members, and why default interface methods were added to C#.
Example: abstraction means hiding details
Abstraction is about what the caller does not need to know. When the school office orders chalk, it fills in an order form. It does not need to know the supplier's warehouse, trucks, or drivers.
Notify is the office: it knows that a sender can reach a guardian and send a message,
nothing more. Email servers, phone gateways, and retry rules all stay hidden inside the
implementations.
Apply it to the SMS
Step 1: Person becomes abstract and soft-deletable
Person is now abstract, has an abstract Role property, and implements the new
ISoftDeletable interface:
namespace SchoolApp.Core.Interfaces;
// Lesson 41: an interface is a contract: what a class can do, with no code of its own.
// Tables with IsDeleted and DeletedAt columns (Lesson 14) implement it.
public interface ISoftDeletable
{
bool IsDeleted { get; }
DateTime? DeletedAt { get; }
void SoftDelete(DateTime deletedAtUtc);
}
using SchoolApp.Core.Interfaces;
namespace SchoolApp.Core.Entities;
// Lesson 39: what students, teachers, and guardians have in common.
// Each still has its own table in SchoolDb; inheritance shares C# code, not tables.
// Lesson 41: abstract (there is no plain "person" in the school) and soft-deletable.
public abstract class Person : ISoftDeletable
{
private string _firstName = "";
private string _lastName = "";
public int Id { get; set; }
public string FirstName
{
get => _firstName;
set => _firstName = RequireText(value, nameof(FirstName));
}
public string LastName
{
get => _lastName;
set => _lastName = RequireText(value, nameof(LastName));
}
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public string FullName => $"{FirstName} {LastName}";
// Lesson 41: every kind of person must say what role it has.
public abstract string Role { get; }
// ISoftDeletable: rows are marked deleted, never removed (Lesson 14).
public bool IsDeleted { get; private set; }
public DateTime? DeletedAt { get; private set; }
public void SoftDelete(DateTime deletedAtUtc)
{
IsDeleted = true;
DeletedAt = deletedAtUtc;
}
// Lesson 40: virtual lets each derived class describe itself in its own way.
public virtual string Describe() => FullName;
// object.ToString() is virtual too; overriding it changes what printing an object shows.
public override string ToString() => Describe();
// protected: usable here and in every class that inherits from Person.
protected static string RequireText(string value, string propertyName)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException($"{propertyName} is required.", propertyName);
}
return value.Trim();
}
}
Student, Teacher, and Guardian each add one line, such as
public override string Role => "Student";. A derived class that forgets it does not build:
error CS0534: 'Librarian' does not implement inherited abstract member 'Person.Role.get'.
Step 2: the sender interface and two implementations
using SchoolApp.Core.Entities;
namespace SchoolApp.Core.Interfaces;
// Lesson 41: what every notification channel must be able to do.
public interface INotificationSender
{
string Channel { get; }
bool CanReach(Guardian guardian);
string Send(Guardian guardian, string message);
// A default interface method: senders get it for free, and may replace it.
string Describe() => $"Sends by {Channel}";
}
Show EmailSender.cs and PhoneSender.cs
using SchoolApp.Core.Entities;
using SchoolApp.Core.Interfaces;
namespace SchoolApp.Core.Notifications;
// Lesson 41: one implementation of INotificationSender. Real sending comes in the Web API modules.
public class EmailSender : INotificationSender
{
public string Channel => "Email";
public bool CanReach(Guardian guardian) => !string.IsNullOrWhiteSpace(guardian.Email);
public string Send(Guardian guardian, string message) => $"[Email to {guardian.Email}] {message}";
}
using SchoolApp.Core.Entities;
using SchoolApp.Core.Interfaces;
namespace SchoolApp.Core.Notifications;
// Lesson 41: a second implementation. Every guardian has a phone number (NOT NULL in SchoolDb).
public class PhoneSender : INotificationSender
{
public string Channel => "Phone";
public bool CanReach(Guardian guardian) => !string.IsNullOrWhiteSpace(guardian.Phone);
public string Send(Guardian guardian, string message) => $"[Text to {guardian.Phone}] {message}";
}
: INotificationSender after the class name means "this class implements the interface".
Leave out a member, and the build fails: error CS0535: 'LetterSender' does not implement interface member 'INotificationSender.Send(Guardian, string)'.
Step 3: the abstract report card
namespace SchoolApp.Core.Reports;
// Lesson 41: an abstract class: shared code plus a step every report card must fill in.
// You cannot create a plain ReportCard, only a PrimaryReportCard or a SecondaryReportCard.
public abstract class ReportCard
{
public string StudentName { get; }
public string ClassName { get; }
protected ReportCard(string studentName, string className)
{
StudentName = studentName;
ClassName = className;
}
public string Heading => $"Report card: {StudentName}, {ClassName}";
// Shared by every report card: the line layout and the absent rule.
public string FormatLine(string subject, decimal? marks, decimal maxMarks)
{
string score = marks is null ? "Absent" : FormatScore(marks.Value, maxMarks);
return $" {subject,-16} {score}";
}
// Abstract: no body here; every derived class must provide one.
protected abstract string FormatScore(decimal marks, decimal maxMarks);
}
// Classes 1 to 5: a descriptive band instead of a number.
public class PrimaryReportCard : ReportCard
{
public PrimaryReportCard(string studentName, string className) : base(studentName, className) { }
protected override string FormatScore(decimal marks, decimal maxMarks)
{
decimal percentage = marks / maxMarks * 100;
return percentage switch
{
>= 81 => "Excellent",
>= 51 => "Good",
_ => "Needs support"
};
}
}
// Classes 6 to 10: the marks themselves.
public class SecondaryReportCard : ReportCard
{
public SecondaryReportCard(string studentName, string className) : base(studentName, className) { }
protected override string FormatScore(decimal marks, decimal maxMarks) => $"{marks:0.##} / {maxMarks:0.##}";
}
Step 4: the program
Show src/SchoolApp.Console/Program.cs
// Lesson 41: abstract classes and interfaces.
using SchoolApp.Core.Entities;
using SchoolApp.Core.Interfaces;
using SchoolApp.Core.Notifications;
using SchoolApp.Core.Reports;
Student aarav = new Student("ADM-2017-0001", "Aarav", "Sharma", new DateOnly(2011, 4, 12));
Teacher anitha = new Teacher("TCH-001", "Anitha", "Rao", "anitha.rao@nexcodingschool.example", "9876540001");
Guardian rajesh = new Guardian("Rajesh", "Sharma", "9876540101") { Email = "rajesh.sharma@nexcodingschool.example" };
Guardian suresh = new Guardian("Suresh", "Patel", "9876540103");
// 1. Person is abstract: every derived class must provide Role.
Person[] people = { aarav, anitha, rajesh };
foreach (Person person in people)
{
Console.WriteLine($"{person.Role,-9} {person.FullName}");
}
// 2. Interfaces as types: try each sender in order; the first that can reach the guardian sends.
INotificationSender[] senders = { new EmailSender(), new PhoneSender() };
foreach (INotificationSender sender in senders)
{
Console.WriteLine(sender.Describe());
}
FeeNotice notice = new FeeNotice(); // FeeNotice decides what to say; a sender decides how
Notify(senders, rajesh, notice.Compose(rajesh, "Aarav", 0m));
Notify(senders, suresh, notice.Compose(suresh, "Diya", 25500m));
// 3. One class, two roles: a Teacher is a Person and an ISoftDeletable.
Teacher rahul = new Teacher("TCH-002", "Rahul", "Verma", "rahul.verma@nexcodingschool.example", "9876540002");
ISoftDeletable leaving = rahul;
leaving.SoftDelete(new DateTime(2026, 7, 31, 10, 0, 0, DateTimeKind.Utc));
Console.WriteLine($"{rahul.FullName} deleted: {rahul.IsDeleted}, at {rahul.DeletedAt:yyyy-MM-dd HH:mm} UTC");
// 4. Abstract ReportCard: shared layout, different scoring per stage.
ReportCard primary = new PrimaryReportCard("Aditya Kapoor", "Class 3-C");
Console.WriteLine(primary.Heading);
Console.WriteLine(primary.FormatLine("Science", 98m, 100m));
Console.WriteLine(primary.FormatLine("Mathematics", 69m, 100m));
Console.WriteLine(primary.FormatLine("Computer Science", 23m, 100m));
ReportCard secondary = new SecondaryReportCard("Meera Iyer", "Class 10-A");
Console.WriteLine(secondary.Heading);
Console.WriteLine(secondary.FormatLine("Mathematics", 95m, 100m));
Console.WriteLine(secondary.FormatLine("Science", null, 100m));
static void Notify(INotificationSender[] senders, Guardian guardian, string message)
{
foreach (INotificationSender sender in senders)
{
if (sender.CanReach(guardian))
{
Console.WriteLine(sender.Send(guardian, message));
return;
}
}
Console.WriteLine($"No way to reach {guardian.FullName}");
}
Student Aarav Sharma
Teacher Anitha Rao
Guardian Rajesh Sharma
Sends by Email
Sends by Phone
[Email to rajesh.sharma@nexcodingschool.example] Dear Rajesh Sharma, thank you: the fees for Aarav are paid in full.
[Text to 9876540103] Dear Suresh Patel, the fee balance for Diya is 25500. Please pay by the due date.
Rahul Verma deleted: True, at 2026-07-31 10:00 UTC
Report card: Aditya Kapoor, Class 3-C
Science Excellent
Mathematics Good
Computer Science Needs support
Report card: Meera Iyer, Class 10-A
Mathematics 95 / 100
Science Absent
Example: trying the senders in order
Notify holds an array of INotificationSender and asks each in turn: can you reach this
guardian? Rajesh has an email, so the email sender answers first. Suresh has none, so the
phone sender takes over.
Notify never mentions EmailSender or PhoneSender. It knows only the interface. Add a
WhatsAppSender tomorrow, put it in the array, and Notify does not change at all.
The order of the array is the school's policy: email first, phone as the fallback. Changing the policy means reordering one line, not rewriting the method.
Example: what to say versus how to deliver
The program uses two separate objects for one notice. FeeNotice, from Lesson 40,
decides what to say. An INotificationSender decides how to deliver it.
Splitting these two jobs means any message can go through any channel. A fee notice, an exam timetable, and a holiday notice can all use the same senders. Lesson 46 calls this the Single Responsibility Principle.
Example: one class, base class plus interface
Teacher inherits from Person, and through it implements ISoftDeletable. So Rahul
Verma is a Teacher, a Person, and an ISoftDeletable at the same time.
ISoftDeletable leaving = rahul; sees only the soft-delete members. That is useful for
code that handles deletion for every kind of record: it needs nothing else. In Lesson 44,
other tables with IsDeleted columns, such as FeePayments and Users, implement it too.
Example: report cards for two stages
Aditya Kapoor is a real Class 3 student in SchoolDb. His Unit Test 1 marks, 98, 69, and 23, become Excellent, Good, and Needs support on the primary card. Meera's secondary card shows her real marks, and her missed Science test shows Absent.
FormatLine, including the absent rule, lives once in the abstract class. Only
FormatScore differs. That is the typical reason to choose an abstract class: real shared
code plus one or two steps that vary.
Common mistakes
- Creating an abstract class.
new Person("A", "B")fails witherror CS0144: Cannot create an instance of the abstract type or interface 'Person'. Fix: create a concrete derived class. - Forgetting an abstract or interface member. CS0534 and CS0535. Fix: implement every member the contract lists.
- Huge interfaces. An
ISchoolServicewith forty methods forces every implementation to provide all of them. Fix: small interfaces with one purpose (Lesson 46). - An interface for everything. An interface with one implementation and no tests adds files, not value. Fix: introduce one when you need a swap, a fake, or a contract.
- Expecting default interface methods on the class variable. Fix: call them through the interface type.
In real projects
The SMS I worked on sent text messages through one provider, called directly from forty places. When the provider raised prices, switching took a month of edits and testing.
On the next system, every message went through an INotificationSender. When we changed
providers there, it was one new class and one line of setup, finished in a day.
That is the real answer to "why interfaces?" They make the expensive decision, which company, which technology, a cheap one to change later.
Best practices
- Use an abstract class for related classes that share real code.
- Use an interface for a capability, especially one that may have several implementations.
- Keep interfaces small and focused on one job.
- Name interfaces with an
Iprefix and a capability:INotificationSender,ISoftDeletable. - Write code against the interface, and choose the implementation in one place.
Recap
- An abstract class cannot be created; abstract members must be overridden.
- An interface is a contract of members, implemented with
: IName. - A class has one base class but can implement many interfaces.
- Default interface methods are reached through the interface type only.
PersonandReportCardare abstract;INotificationSenderandISoftDeletableare interfaces.- Choose an abstract class for shared code, an interface for a shared capability.
Homework
Library module task: Make LibraryItem abstract with an abstract LoanDays. Write an
IReservable interface with ReservedFor and Reserve(studentName).
Book inherits from LibraryItem and implements IReservable; Magazine only inherits.
For each item, print the due date, then reserve it for Kabir Singh if it is reservable.
A correct result looks like:
The Jungle Book due 2026-07-20
reserved for Kabir Singh
Young Scientist due 2026-07-13
cannot be reserved
Show the solution
// Library homework, Lesson 41: an abstract LibraryItem and an IReservable interface.
// Run with: dotnet run library-assignment/041-library-abstract-interface.cs
DateOnly issuedOn = new DateOnly(2026, 7, 6);
LibraryItem[] items = { new Book("The Jungle Book"), new Magazine("Young Scientist") };
foreach (LibraryItem item in items)
{
Console.WriteLine($"{item.Title,-16} due {item.GetDueDate(issuedOn):yyyy-MM-dd}");
if (item is IReservable reservable)
{
reservable.Reserve("Kabir Singh");
Console.WriteLine($"{"",-16} reserved for {reservable.ReservedFor}");
}
else
{
Console.WriteLine($"{"",-16} cannot be reserved");
}
}
abstract class LibraryItem
{
public string Title { get; }
protected LibraryItem(string title)
{
Title = title;
}
public abstract int LoanDays { get; }
public DateOnly GetDueDate(DateOnly issuedOn) => issuedOn.AddDays(LoanDays);
}
interface IReservable
{
string? ReservedFor { get; }
void Reserve(string studentName);
}
class Book : LibraryItem, IReservable
{
public Book(string title) : base(title) { }
public override int LoanDays => 14;
public string? ReservedFor { get; private set; }
public void Reserve(string studentName) => ReservedFor = studentName;
}
class Magazine : LibraryItem
{
public Magazine(string title) : base(title) { }
public override int LoanDays => 7;
}
item is IReservable reservable checks for the capability, not for a class. Any future item
that implements IReservable works without changing the loop.
Quiz
What happens with new Person(...) when Person is abstract?
Show answer
Answer: B. Compile error CS0144 Abstract classes cannot be created; only their derived classes can.
How many interfaces can one class implement?
Show answer
Answer: C. Many One base class, any number of interfaces.
Which fits 'related classes that share real code plus one varying step'?
Show answer
Answer: B. An abstract class ReportCard shares FormatLine and leaves FormatScore abstract.
Why does Notify work for a future WhatsAppSender without changes?
Show answer
Answer: B. It depends only on the INotificationSender interface Any class implementing the interface can be put in the array.
Where can a default interface method be called from?
Show answer
Answer: A. Only through the interface type On the class variable it is not visible (CS1061).
Interview questions
Questions are grouped by experience level. Click a question to see a model answer. All questions: OOP interview questions.
Fresher (0-2 yrs)
F What is an abstract class? concept
A class marked abstract that cannot be created with new, only inherited from. It can hold
shared code and abstract members without a body, which every derived class must override, such
as ReportCard.FormatScore.
F What is an interface? concept
A contract listing members a class promises to provide, such as INotificationSender with
Channel, CanReach, and Send. It has no constructors or instance fields; a class implements
it with : IName and must provide every member.
F Can a class implement more than one interface? concept
Yes, any number, while having only one base class. A Teacher inherits from Person and is
also an ISoftDeletable; a class could implement several capability interfaces at once.
Mid (3-5 yrs)
M What is the difference between an abstract class and an interface, and when do you use each? concept
An abstract class can hold fields, constructors, and shared code, and a class can have only one; it models "is a" for related classes. An interface is a contract a class can implement alongside others; it models "can do". Use an abstract class for shared code, an interface for a capability with possibly unrelated implementations.
M What are default interface methods? concept
Since C# 8, an interface member can have a body that implementing classes inherit, such as
Describe() in INotificationSender. They are reachable only through the interface type, and
they let you add members to an interface without breaking existing implementations.
M What is abstraction in OOP? concept
Depending on what something does, not how it does it, and hiding the details. Notify knows
only that a sender can reach a guardian and send a message; email servers and phone gateways
stay hidden in the implementations.
Senior (6-9 yrs)
S The SMS calls a text-message provider directly from forty places, and the school wants to switch providers. What would you do? scenario
Introduce an interface such as INotificationSender, wrap the current provider in one class
that implements it, and change the forty call sites to use the interface. Then the new provider
is one new class plus one line of setup, and both can be tested with fakes.
How to answer: Describe the refactoring steps safely (introduce the interface, migrate callers gradually, keep behavior identical), then the payoff: vendor independence and testability.
S Should every class have an interface? design
No. Add an interface when you need to swap implementations, fake a dependency in tests, or define a contract between teams or layers. An interface with one implementation and no such need only adds files and indirection.
How to answer: Show judgment: name concrete reasons to add an interface and the cost of adding them everywhere. Mention that dependency injection in ASP.NET Core makes interfaces cheap where they are useful.
Architect / Lead (10+ yrs)
A How do interfaces and abstract classes differ when a public contract must evolve over years? design
Adding an abstract member to either breaks every implementation. Adding a normal or virtual method to an abstract class, or a default method to an interface, does not. Design contracts small and stable, version them deliberately, and prefer adding new interfaces over changing widely used ones.
How to answer: Discuss compatibility for libraries and APIs, the role of default interface methods, and strategies such as new interfaces, adapters, and versioning.
Resources
- Code for this lesson: git tag
lesson-41-abstract-interfaces(code repository coming soon) - Files:
src/SchoolApp.Core/Entities/Person.cs,src/SchoolApp.Core/Interfaces/ISoftDeletable.cs,src/SchoolApp.Core/Interfaces/INotificationSender.cs,src/SchoolApp.Core/Reports/ReportCard.cs,src/SchoolApp.Console/Program.cs,library-assignment/041-library-abstract-interface.cs - Video: coming soon
What's next
SchoolRules has been a static class since Lesson 34, without a full explanation. Next:
static, sealed, and partial, three keywords that control how classes are used.
Lesson 42: static, sealed, and partial
Want live sessions, code reviews, and interview preparation? Join the next batch at Sahasra Technologies.