Backend Development with .NET
Session 06
OOP II: Polymorphism,
Abstraction & Interfaces
Eng. Seif Mansour  ·  Andalusia Academy
Week 3  ·  2 hours
Session Goals

By the end of this session, you will be able to:

  • Explain and apply both forms of polymorphism
  • Define and use abstract classes
  • Define and implement interfaces
  • Choose between an abstract class and an interface for a given design problem
Agenda
TimeSegmentTypeDuration
0:00PolymorphismTheory + Demo20 min
0:20Abstract classesTheory + Demo20 min
0:40InterfacesTheory + Demo25 min
1:05Break5 min
1:10Abstract class vs interfaceDiscussion + Demo20 min
1:30Common .NET interfacesTheory + Demo15 min
1:45LabLab15 min
Polymorphism
"Many forms" — the same operation behaves differently depending on the type it acts on. Two kinds: decided at compile time, or at runtime.
In this section
  • Compile-time polymorphism (overloading)
  • Runtime polymorphism (overriding)
  • Type checking with is
  • Safe casting with as
Two Forms of Polymorphism
  • Compile-time — method overloading. The compiler selects which method to call based on the argument types at compile time. You covered this in Session 04.
  • Runtime — method overriding with virtual / override. The runtime selects which implementation to execute based on the object's actual type. Covered in Session 05.
  • Both forms let you write code that works on many types through a single unified interface
  • Runtime polymorphism is the more powerful form — it enables collections of mixed types to be processed uniformly
Key distinction
Overloading is resolved by the compiler. Overriding is resolved by the runtime — that is why it is called runtime polymorphism.
Runtime Polymorphism in Action
Program.cs
Shape[] shapes = { new Circle(5), new Rectangle(3, 4) };

foreach (Shape s in shapes)
{
    Console.WriteLine(s.Area());   // correct override called at runtime
}

// The variable type is Shape, but the actual object decides which Area() runs

The array holds Shape references — but each call to Area() dispatches to the override in the actual concrete type.

Type Checking & Casting
  • is checks whether an object is of a given type and, with pattern matching, binds it to a typed variable in one step
  • as attempts a cast and returns null on failure — never throws
  • Prefer the is pattern — it is safer and more readable than a cast that can throw InvalidCastException
Shape s = new Circle(5);

// Pattern matching — checks and binds in one step
if (s is Circle c)
{
    Console.WriteLine($"Radius: {c.Radius}");
}

// 'as' operator — returns null instead of throwing
Circle? circle = s as Circle;
if (circle != null)
{
    Console.WriteLine(circle.Radius);
}
Abstract Classes
A class that exists only to be a base — it cannot be instantiated directly. It defines what derived classes must implement while sharing reusable logic.
In this section
  • abstract keyword
  • Abstract methods — no body, must be overridden
  • Concrete methods — shared logic for all derived classes
  • Polymorphic dispatch through the base type
Abstract Classes
  • Marked with the abstract keyword — the compiler prevents direct instantiation
  • Can contain abstract methods: declared with no body; every non-abstract derived class must provide an implementation
  • Can also contain concrete methods: fully implemented, shared by all derived classes without repetition
  • Can have constructors, fields, and properties — just like a normal class
  • Models an IS-A relationship where the base type is genuinely incomplete on its own
Note
new AbstractClass() is a compile error. Abstract classes live only as base types.
Abstract Class — Notification (Part 1 of 2)
Notifications/Notification.cs
public abstract class Notification
{
    public string Recipient { get; set; }
    public string Message   { get; set; }

    public Notification(string recipient, string message)
    {
        Recipient = recipient;
        Message   = message;
    }

    // No body — derived classes must provide this
    public abstract void Send();

    // Shared logic — available to all derived classes
    public void Log()
    {
        Console.WriteLine($"[LOG] Sending to {Recipient}: {Message}");
    }
}
Abstract Class — Notification (Part 2 of 2)
Notifications/EmailNotification.cs
public class EmailNotification : Notification
{
    public EmailNotification(string recipient, string message)
        : base(recipient, message) { }

    public override void Send()
        => Console.WriteLine($"Email to {Recipient}: {Message}");
}

public class SmsNotification : Notification
{
    public SmsNotification(string recipient, string message)
        : base(recipient, message) { }

    public override void Send()
        => Console.WriteLine($"SMS to {Recipient}: {Message}");
}

Both classes must implement Send() — the compiler enforces it. They inherit Log() for free.

Using Abstract Classes
Program.cs
List<Notification> queue = new List<Notification>
{
    new EmailNotification("alice@example.com", "Welcome!"),
    new SmsNotification("+1234567890", "Your code: 9876")
};

foreach (var n in queue)
{
    n.Log();    // concrete — shared method runs for both
    n.Send();   // abstract — polymorphic dispatch to the correct override
}

The list type is Notification. The runtime dispatches Send() to the correct subclass — the loop does not need to know which.

Interfaces
A contract — a set of members that any implementing class must provide. No fields, no constructors. A class can implement many interfaces at once.
In this section
  • Defining an interface
  • Implementing an interface
  • Multiple-interface implementation
  • Programming to the interface type
Interfaces
  • Declared with the interface keyword; by convention the name starts with I
  • Contains only member signatures — no fields, no constructors, no instance state (by default)
  • A class that implements an interface must provide all declared members
  • A class can implement multiple interfaces — overcoming C#'s single-inheritance limit
  • The caller works through the interface type and never depends on the concrete class
C# 8+
Interfaces may have default method implementations, but in practice you will rarely use them. Treat interfaces as pure contracts until you have a specific reason otherwise.
Defining & Implementing an Interface

The interface — pure contract:

public interface IPaymentProcessor
{
    bool   ProcessPayment(decimal amount);
    void   Refund(decimal amount);
    string ProviderName { get; }
}

Any class implementing this interface must supply all three members.

Implementation:

public class StripeProcessor : IPaymentProcessor
{
    public string ProviderName => "Stripe";

    public bool ProcessPayment(decimal amount)
    {
        Console.WriteLine(
            $"Processing ${amount} via Stripe");
        return true;
    }

    public void Refund(decimal amount)
        => Console.WriteLine(
               $"Refunding ${amount} via Stripe");
}
Implementing Multiple Interfaces
  • A class separates interface names with commas after the colon
  • All members of every listed interface must be implemented
  • This is how C# achieves the flexibility of multiple inheritance without its ambiguity problems
Real-world pattern
You will regularly see classes implement both a domain interface (e.g., IRepository) and a cross-cutting one (e.g., IDisposable).
public interface ILoggable
{
    void LogActivity(string action);
}

// Implements both interfaces
public class StripeProcessor
    : IPaymentProcessor, ILoggable
{
    public string ProviderName => "Stripe";

    public bool ProcessPayment(decimal amount)
    { /* ... */ return true; }

    public void Refund(decimal amount) { /* ... */ }

    public void LogActivity(string action)
        => Console.WriteLine($"[Stripe] {action}");
}
Programming to the Interface
Services/CheckoutService.cs
static void Checkout(IPaymentProcessor processor, decimal amount)
{
    processor.ProcessPayment(amount);
}

// The caller decides which implementation to inject
Checkout(new StripeProcessor(), 99.99m);
Checkout(new PayPalProcessor(), 49.50m);

Checkout depends only on the contract, never on StripeProcessor or PayPalProcessor directly. Swapping providers requires no change to the method.

Preview
This is exactly how .NET dependency injection works: you register IPaymentProcessor → StripeProcessor in the service container. You will use this from Session 08 onwards.
Key Concept
"Depend on abstractions, not on concretions. An interface is a promise — the caller binds to the promise, not to who keeps it."
— Interfaces  ·  Session 06
Abstract Class vs Interface
Both enforce a contract. Choosing the right one depends on whether you are modelling shared identity and state, or a portable capability.
In this section
  • Feature comparison table
  • IS-A vs CAN-DO rule of thumb
  • When each is the right choice
Abstract Class vs Interface — Features
FeatureAbstract classInterface
Can have fieldsYesNo
Can have constructorsYesNo
Can have concrete methodsYesYes (C# 8+ default methods)
Multiple inheritanceNo — only one base classYes — implement many interfaces
ModelsIS-A relationship with shared stateCAN-DO capability across any type
Rule of Thumb
  • IS-A relationship with shared state? Use an abstract class. All Animal subclasses share a Name field and a base constructor — that shared identity and data belongs in an abstract class.
  • CAN-DO capability across unrelated types? Use an interface. Both a Dog and a Car can implement ISerializable — they have nothing in common except the capability.
  • When in doubt: prefer an interface. It imposes no inheritance hierarchy and leaves the implementing class free to inherit from something else.
Discussion
"You have ILogger and BaseLogger. Which do you use and why?" — consider what callers need to know and what code can be shared.
Common .NET Interfaces
The .NET standard library is built on interfaces. Knowing the most common ones makes it immediately clear why a type behaves the way it does.
In this section
  • IComparable<T> — natural sort order
  • IDisposable — resource cleanup
  • IEnumerable<T> — iteration support
IComparable<T> — Natural Sort Order
  • Implementing IComparable<T> gives a type a natural ordering
  • CompareTo returns a negative number, zero, or a positive number to indicate less-than, equal, or greater-than
  • List<T>.Sort() and Array.Sort() call CompareTo automatically when the element type implements this interface
public class Student : IComparable<Student>
{
    public string Name { get; set; }
    public double GPA  { get; set; }

    // Sort ascending by GPA
    public int CompareTo(Student other)
        => GPA.CompareTo(other.GPA);
}

var students = new List<Student> { ... };
students.Sort();   // uses CompareTo
IDisposable — Resource Cleanup
IO/FileWriter.cs
public class FileWriter : IDisposable
{
    private StreamWriter _writer;

    public FileWriter(string path)
        => _writer = new StreamWriter(path);

    public void Write(string text)
        => _writer.WriteLine(text);

    public void Dispose() => _writer?.Dispose();
}

// The 'using' statement calls Dispose() automatically at the closing brace
using (var writer = new FileWriter("log.txt"))
{
    writer.Write("Hello");
}   // Dispose() runs here — file handle released even if an exception occurs

Implement IDisposable whenever your class holds unmanaged resources: files, database connections, network sockets.

IEnumerable<T> — Iteration Support
  • Any type that implements IEnumerable<T> can be used in a foreach loop
  • List<T>, arrays, Queue<T>, Stack<T> — all implement it; that is why foreach works on all of them uniformly
  • LINQ (Where, Select, OrderBy, ...) operates on IEnumerable<T> — write it once, works on any collection
  • When a method returns IEnumerable<T> instead of List<T>, callers only depend on the ability to iterate — the internal representation can change freely
You will see this everywhere
EF Core query results, repository method return types, and LINQ chains all use IEnumerable<T> or its async counterpart IAsyncEnumerable<T>.
Lab — IPrintable
  1. Define an interface IPrintable with one method: void Print()
  2. Create a Book class with Title and Author properties that implements IPrintablePrint() writes "Book: {Title} by {Author}"
  3. Create a Movie class with Title and Year properties that implements IPrintablePrint() writes "Movie: {Title} ({Year})"
  4. Create a List<IPrintable> containing several books and movies, loop over it, and call Print() on each item
Summary
  • Polymorphism comes in two forms: compile-time (overloading) and runtime (overriding) — the runtime form is the more powerful design tool
  • is pattern matching and the as operator provide safe type checking and casting without exceptions
  • An abstract class defines a partial contract — it can hold state and shared logic, but cannot be instantiated directly
  • An interface defines a pure contract — no state, no constructors, but a class can implement many at once
  • Choose abstract class for IS-A with shared state; choose interface for CAN-DO capabilities across unrelated types
  • IComparable<T>, IDisposable, and IEnumerable<T> are three fundamental .NET interfaces you will encounter constantly
What's Next

Next session: Session 07 — C# Language Features: Enums, Generics, Collections & Delegates

  • Enums for strongly-typed named constants
  • Generic classes and methods — write once, work on any type
  • List<T>, Dictionary<K,V>, Queue<T>, Stack<T>
  • Delegates and lambda expressions — first-class functions in C#
Before next session
Complete the lab and the assignment. Session 07 will build on interfaces when we introduce generics and typed collections.
Assignment

Build a shape rendering system that exercises abstract classes, interfaces, and polymorphism.

  • Define an interface IDrawable with a method void Draw()
  • Define an abstract class Shape with an abstract double Area() method and a concrete void Describe() method that prints the class name and area
  • Create Circle and Rectangle that extend Shape and implement IDrawable — each Draw() prints a short ASCII representation
  • Store several shapes in a Shape[] and loop over them calling both Describe() and Draw()
  • Add a Triangle class and verify that everything still compiles and runs without changing the loop
Bonus
Define a second interface IResizable with void Scale(double factor). Implement it on Circle and Rectangle. Write a method that accepts IEnumerable<IResizable> and scales every element by a given factor.
Questions?
Session 06  ·  OOP II: Polymorphism, Abstraction & Interfaces