Skip to main content

Abstract Classes vs Interfaces: Report Cards and Senders – C# | Lesson 41

Published / updated

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​

Example
public abstract class ReportCard
{
public string FormatLine(string subject, decimal? marks, decimal maxMarks) { /* shared code */ }

protected abstract string FormatScore(decimal marks, decimal maxMarks);
}
PartMeaning
abstract classCannot be created with new; only inherited from
Normal membersShared code that every derived class gets
abstract memberNo 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​

Example
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 classInterface
Can contain codeYes: fields, constructors, normal methodsOnly default methods, no instance fields
A class can haveOne base classMany interfaces
Relationship"is a" (a primary report card is a report card)"can do" (an email sender can send notifications)
MembersAny access level, including protectedPublic by default
Best forRelated classes that share codeA 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:

InterfaceGives a class the ability toYou used it when
IEnumerable<T>Be walked through with foreachLooping over arrays (Lesson 32)
IComparable<T>Be sortedSorting comes in Module 8
IDisposableRelease files and connections with usingDatabase connections, Module 9
IEquatable<T>Compare by valueRecords, 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 toExisting implementations
An interface, without a bodyAll stop compiling until they implement it
An interface, with a default bodyKeep working; they get the default
An abstract class, as a normal or virtual methodKeep working; they inherit it
An abstract class, as an abstract memberAll 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:

src/SchoolApp.Core/Interfaces/ISoftDeletable.cs
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);
}
src/SchoolApp.Core/Entities/Person.cs
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​

src/SchoolApp.Core/Interfaces/INotificationSender.cs
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
src/SchoolApp.Core/Notifications/EmailSender.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}";
}
src/SchoolApp.Core/Notifications/PhoneSender.cs
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​

src/SchoolApp.Core/Reports/ReportCard.cs
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
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}");
}
Output
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​

  1. Creating an abstract class. new Person("A", "B") fails with error CS0144: Cannot create an instance of the abstract type or interface 'Person'. Fix: create a concrete derived class.
  2. Forgetting an abstract or interface member. CS0534 and CS0535. Fix: implement every member the contract lists.
  3. Huge interfaces. An ISchoolService with forty methods forces every implementation to provide all of them. Fix: small interfaces with one purpose (Lesson 46).
  4. 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.
  5. Expecting default interface methods on the class variable. Fix: call them through the interface type.

In real projects​

From 16 years of .NET 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 I prefix 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.
  • Person and ReportCard are abstract; INotificationSender and ISoftDeletable are 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:

Output
The Jungle Book due 2026-07-20
reserved for Kabir Singh
Young Scientist due 2026-07-13
cannot be reserved
Show the solution
library-assignment/041-library-abstract-interface.cs
// 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​

  1. What happens with new Person(...) when Person is abstract?

    1. It creates a Person
    2. Compile error CS0144
    3. It creates a Student
    4. A runtime exception
    Show answer

    Answer: B. Compile error CS0144 Abstract classes cannot be created; only their derived classes can.

  2. How many interfaces can one class implement?

    1. None
    2. One
    3. Many
    4. Two at most
    Show answer

    Answer: C. Many One base class, any number of interfaces.

  3. Which fits 'related classes that share real code plus one varying step'?

    1. An interface
    2. An abstract class
    3. A static class
    4. An enum
    Show answer

    Answer: B. An abstract class ReportCard shares FormatLine and leaves FormatScore abstract.

  4. Why does Notify work for a future WhatsAppSender without changes?

    1. It uses a switch
    2. It depends only on the INotificationSender interface
    3. It is static
    4. WhatsApp is built into .NET
    Show answer

    Answer: B. It depends only on the INotificationSender interface Any class implementing the interface can be put in the array.

  5. Where can a default interface method be called from?

    1. Only through the interface type
    2. Only through the class type
    3. Anywhere
    4. Only in the interface
    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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.