πŸ“„

Interfaces Abstract Classes

Intermediate 4 min read 700 words

Interfaces and Abstract Classes in C#

Introduction

Interfaces and abstract classes are fundamental tools for abstraction in C#. Understanding when to use each, and how they’ve evolved in modern C#, is essential for designing flexible and maintainable systems.


Table of Contents


Interface Fundamentals

An interface defines a contract - what a type can do, without specifying how.

Basic Interface Definition

// Interface defines capabilities
public interface IPaymentProcessor
{
    // Method signatures
    Task<PaymentResult> ProcessAsync(decimal amount);
    Task<RefundResult> RefundAsync(string transactionId);

    // Properties
    string ProviderName { get; }
    bool IsAvailable { get; }

    // Events
    event EventHandler<PaymentEventArgs> PaymentCompleted;
}

// Implementation
public class StripePaymentProcessor : IPaymentProcessor
{
    public string ProviderName => "Stripe";
    public bool IsAvailable => true;

    public event EventHandler<PaymentEventArgs> PaymentCompleted;

    public async Task<PaymentResult> ProcessAsync(decimal amount)
    {
        // Stripe-specific implementation
        var result = new PaymentResult { Success = true };
        PaymentCompleted?.Invoke(this, new PaymentEventArgs(result));
        return result;
    }

    public async Task<RefundResult> RefundAsync(string transactionId)
    {
        // Stripe-specific refund logic
        return new RefundResult { Success = true };
    }
}

Interface Inheritance

// Interfaces can inherit from other interfaces
public interface IEntity
{
    int Id { get; }
}

public interface IAuditable
{
    DateTime CreatedAt { get; }
    DateTime? ModifiedAt { get; }
    string CreatedBy { get; }
    string? ModifiedBy { get; }
}

public interface ISoftDeletable
{
    bool IsDeleted { get; }
    DateTime? DeletedAt { get; }
}

// Combined interface
public interface IAuditableEntity : IEntity, IAuditable, ISoftDeletable
{
    // Inherits all members from parent interfaces
}

// Implementing class must satisfy all interfaces
public class Document : IAuditableEntity
{
    public int Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? ModifiedAt { get; set; }
    public string CreatedBy { get; set; }
    public string? ModifiedBy { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }

    public string Title { get; set; }
    public string Content { get; set; }
}

Abstract Class Fundamentals

An abstract class provides a partial implementation that derived classes complete.

Basic Abstract Class

public abstract class Shape
{
    // Fields can have any access modifier
    protected string _name;

    // Constructor (called by derived classes)
    protected Shape(string name)
    {
        _name = name;
    }

    // Abstract members - must be implemented
    public abstract double CalculateArea();
    public abstract double CalculatePerimeter();

    // Virtual members - can be overridden
    public virtual void Display()
    {
        Console.WriteLine($"Shape: {_name}");
        Console.WriteLine($"Area: {CalculateArea():F2}");
        Console.WriteLine($"Perimeter: {CalculatePerimeter():F2}");
    }

    // Concrete members - shared implementation
    public string GetName() => _name;
}

public class Circle : Shape
{
    public double Radius { get; }

    public Circle(double radius) : base("Circle")
    {
        Radius = radius;
    }

    // Must implement abstract members
    public override double CalculateArea() => Math.PI * Radius * Radius;
    public override double CalculatePerimeter() => 2 * Math.PI * Radius;

    // Optionally override virtual members
    public override void Display()
    {
        base.Display();
        Console.WriteLine($"Radius: {Radius:F2}");
    }
}

Abstract Properties

public abstract class Vehicle
{
    // Abstract property - no implementation
    public abstract int NumberOfWheels { get; }

    // Abstract property with getter and setter
    public abstract string LicensePlate { get; set; }

    // Virtual property - has default implementation
    public virtual int MaxSpeed => 100;

    // Concrete property
    public string Manufacturer { get; set; }
}

public class Car : Vehicle
{
    private string _licensePlate;

    public override int NumberOfWheels => 4;

    public override string LicensePlate
    {
        get => _licensePlate;
        set => _licensePlate = value?.ToUpper();
    }

    public override int MaxSpeed => 200;
}

public class Motorcycle : Vehicle
{
    public override int NumberOfWheels => 2;

    public override string LicensePlate { get; set; }

    public override int MaxSpeed => 250;
}

Decision Matrix

When to Use Interface vs Abstract Class

Scenario Interface Abstract Class
Define a contract only βœ… ❌
Share code implementation ❌ (use DIM*) βœ…
Multiple inheritance βœ… ❌
Non-public members ❌ βœ…
Fields/state ❌ βœ…
Constructors ❌ βœ…
Unrelated classes share behavior βœ… ❌
Related classes in hierarchy ❌ βœ…
Versioning/adding members βœ… (DIM*) ⚠️ Risk breaking

*DIM = Default Interface Methods (C# 8+)

Quick Decision Guide

Does the abstraction represent:
β”œβ”€β”€ A capability/behavior? β†’ Interface (IDisposable, IComparable)
β”‚   Examples: Can be serialized, Can be compared, Can send notifications
β”‚
β”œβ”€β”€ A base type in a hierarchy? β†’ Abstract Class
β”‚   Examples: Shape β†’ Circle/Rectangle, Animal β†’ Dog/Cat
β”‚
β”œβ”€β”€ Something unrelated classes share? β†’ Interface
β”‚   Examples: Both FileLogger and DatabaseLogger can log
β”‚
└── Something that needs shared state/code? β†’ Abstract Class
    Examples: Base repository with connection string

Real-World Example

// Interface: Defines what can be done (capability)
public interface INotificationSender
{
    Task SendAsync(string recipient, string message);
}

// Abstract class: Shares implementation among related types
public abstract class NotificationSenderBase : INotificationSender
{
    protected readonly ILogger _logger;
    protected readonly IConfiguration _config;

    protected NotificationSenderBase(ILogger logger, IConfiguration config)
    {
        _logger = logger;
        _config = config;
    }

    // Template method pattern
    public async Task SendAsync(string recipient, string message)
    {
        _logger.LogInformation("Sending notification to {Recipient}", recipient);

        try
        {
            await SendCoreAsync(recipient, message);
            _logger.LogInformation("Notification sent successfully");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send notification");
            throw;
        }
    }

    // Derived classes implement the actual sending
    protected abstract Task SendCoreAsync(string recipient, string message);
}

// Concrete implementations
public class EmailSender : NotificationSenderBase
{
    private readonly ISmtpClient _smtpClient;

    public EmailSender(ISmtpClient smtpClient, ILogger logger, IConfiguration config)
        : base(logger, config)
    {
        _smtpClient = smtpClient;
    }

    protected override async Task SendCoreAsync(string recipient, string message)
    {
        await _smtpClient.SendEmailAsync(recipient, "Notification", message);
    }
}

public class SmsSender : NotificationSenderBase
{
    private readonly ITwilioClient _twilioClient;

    public SmsSender(ITwilioClient twilioClient, ILogger logger, IConfiguration config)
        : base(logger, config)
    {
        _twilioClient = twilioClient;
    }

    protected override async Task SendCoreAsync(string recipient, string message)
    {
        await _twilioClient.SendSmsAsync(recipient, message);
    }
}

Modern Interface Features

Default Interface Methods (C# 8+)

public interface ILogger
{
    void Log(string message);

    // Default implementation - optional to override
    void LogError(string message) => Log($"ERROR: {message}");
    void LogWarning(string message) => Log($"WARNING: {message}");
    void LogInfo(string message) => Log($"INFO: {message}");

    // Can call other members
    void LogException(Exception ex) => LogError($"{ex.GetType().Name}: {ex.Message}");
}

public class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine(message);

    // Uses default implementations for LogError, LogWarning, LogInfo
}

public class FileLogger : ILogger
{
    private readonly string _path;

    public FileLogger(string path) => _path = path;

    public void Log(string message) => File.AppendAllText(_path, message + "\n");

    // Override default implementation
    public void LogError(string message)
    {
        Log($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] ERROR: {message}");
    }
}

Static Abstract Members (C# 11+)

// Interface can require static members
public interface IParseable<TSelf> where TSelf : IParseable<TSelf>
{
    static abstract TSelf Parse(string s);
    static abstract bool TryParse(string s, out TSelf result);
}

public readonly struct Point : IParseable<Point>
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    // Implement static abstract members
    public static Point Parse(string s)
    {
        var parts = s.Trim('(', ')').Split(',');
        return new Point(int.Parse(parts[0]), int.Parse(parts[1]));
    }

    public static bool TryParse(string s, out Point result)
    {
        try
        {
            result = Parse(s);
            return true;
        }
        catch
        {
            result = default;
            return false;
        }
    }
}

// Generic method using static abstract
public static T ParseAny<T>(string input) where T : IParseable<T>
{
    return T.Parse(input);  // Calls static method on T
}

var point = ParseAny<Point>("(10, 20)");

Interface with Properties and Accessors

public interface IReadOnlyEntity
{
    int Id { get; }
    string Name { get; }
}

public interface IEntity : IReadOnlyEntity
{
    new string Name { get; set; }  // Adds setter
}

// Init-only properties in interfaces (C# 9+)
public interface IImmutableEntity
{
    int Id { get; init; }
    string Name { get; init; }
}

Multiple Interface Implementation

Implementing Multiple Interfaces

public interface ISerializable
{
    byte[] Serialize();
}

public interface ICloneable<T>
{
    T Clone();
}

public interface IComparable<T>
{
    int CompareTo(T other);
}

public interface IEquatable<T>
{
    bool Equals(T other);
}

// Class implements all relevant interfaces
public class Product : ISerializable, ICloneable<Product>, IComparable<Product>, IEquatable<Product>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    public byte[] Serialize()
    {
        return JsonSerializer.SerializeToUtf8Bytes(this);
    }

    public Product Clone()
    {
        return new Product { Id = Id, Name = Name, Price = Price };
    }

    public int CompareTo(Product other)
    {
        return Price.CompareTo(other?.Price ?? 0);
    }

    public bool Equals(Product other)
    {
        return Id == other?.Id;
    }
}

Interface Composition

// Build up capabilities through interface composition
public interface IReadable
{
    string Read();
}

public interface IWritable
{
    void Write(string content);
}

public interface IReadWrite : IReadable, IWritable { }

public interface ISeekable
{
    long Position { get; set; }
    void Seek(long offset);
}

public interface IStream : IReadWrite, ISeekable, IDisposable { }

// Implement exactly what you need
public class MemoryStream : IStream
{
    private byte[] _buffer;
    private long _position;

    public long Position
    {
        get => _position;
        set => _position = value;
    }

    public string Read() => Encoding.UTF8.GetString(_buffer, (int)_position, _buffer.Length - (int)_position);
    public void Write(string content) => _buffer = Encoding.UTF8.GetBytes(content);
    public void Seek(long offset) => _position = offset;
    public void Dispose() => _buffer = null;
}

public class ReadOnlyStream : IReadable
{
    public string Read() => "data";
    // Doesn't implement write capabilities
}

Explicit Interface Implementation

Use when you need to hide interface members or resolve naming conflicts.

Hiding Interface Members

public interface ICollection<T>
{
    void Add(T item);
    bool Remove(T item);
    void Clear();
    int Count { get; }
}

public class ReadOnlyCollection<T> : ICollection<T>
{
    private readonly List<T> _items;

    public ReadOnlyCollection(IEnumerable<T> items)
    {
        _items = items.ToList();
    }

    // Public members
    public int Count => _items.Count;

    // Hidden - only accessible through interface
    void ICollection<T>.Add(T item) =>
        throw new NotSupportedException("Collection is read-only");

    bool ICollection<T>.Remove(T item) =>
        throw new NotSupportedException("Collection is read-only");

    void ICollection<T>.Clear() =>
        throw new NotSupportedException("Collection is read-only");
}

// Usage
var collection = new ReadOnlyCollection<int>(new[] { 1, 2, 3 });
Console.WriteLine(collection.Count);  // Works

// collection.Add(4);  // ERROR: Add is hidden

// Must cast to interface to access
((ICollection<int>)collection).Add(4);  // Throws NotSupportedException

Resolving Naming Conflicts

public interface IAmericanDateFormatter
{
    string Format(DateTime date);  // Returns MM/dd/yyyy
}

public interface IEuropeanDateFormatter
{
    string Format(DateTime date);  // Returns dd/MM/yyyy
}

public class DateFormatter : IAmericanDateFormatter, IEuropeanDateFormatter
{
    // Explicit implementations for each interface
    string IAmericanDateFormatter.Format(DateTime date) => date.ToString("MM/dd/yyyy");
    string IEuropeanDateFormatter.Format(DateTime date) => date.ToString("dd/MM/yyyy");

    // Public method with default behavior
    public string Format(DateTime date, string culture = "en-US")
    {
        return culture switch
        {
            "en-US" => ((IAmericanDateFormatter)this).Format(date),
            "en-GB" or "de-DE" => ((IEuropeanDateFormatter)this).Format(date),
            _ => date.ToString("yyyy-MM-dd")
        };
    }
}

// Usage
var formatter = new DateFormatter();
var date = new DateTime(2024, 12, 25);

Console.WriteLine(formatter.Format(date));                           // 12/25/2024 (default)
Console.WriteLine(((IAmericanDateFormatter)formatter).Format(date)); // 12/25/2024
Console.WriteLine(((IEuropeanDateFormatter)formatter).Format(date)); // 25/12/2024

Explicit Implementation Best Practices

// βœ… Use when:
// 1. Interface has large number of members, but only some are commonly used
// 2. Implementing multiple interfaces with same member signatures
// 3. Interface members don't make sense as part of class's public API

// ❌ Avoid when:
// 1. Members are commonly used - users shouldn't need to cast
// 2. Makes code harder to discover and use

Interface Versioning

Problem: Adding Members Breaks Implementations

// Version 1
public interface IRepository<T>
{
    T GetById(int id);
    void Add(T entity);
}

// Version 2 - Adding new member breaks existing implementations!
public interface IRepository<T>
{
    T GetById(int id);
    void Add(T entity);
    void AddRange(IEnumerable<T> entities);  // Breaking change!
}

Solution 1: Default Interface Methods

public interface IRepository<T>
{
    T GetById(int id);
    void Add(T entity);

    // Default implementation - doesn't break existing code
    void AddRange(IEnumerable<T> entities)
    {
        foreach (var entity in entities)
            Add(entity);
    }
}

Solution 2: Interface Segregation

// Original interface stays unchanged
public interface IRepository<T>
{
    T GetById(int id);
    void Add(T entity);
}

// New capability in separate interface
public interface IBulkRepository<T>
{
    void AddRange(IEnumerable<T> entities);
}

// New implementations can implement both
public class SqlRepository<T> : IRepository<T>, IBulkRepository<T>
{
    public T GetById(int id) => /* ... */;
    public void Add(T entity) => /* ... */;
    public void AddRange(IEnumerable<T> entities) => /* ... */;
}

Solution 3: Versioned Interfaces

public interface IRepositoryV1<T>
{
    T GetById(int id);
    void Add(T entity);
}

public interface IRepositoryV2<T> : IRepositoryV1<T>
{
    void AddRange(IEnumerable<T> entities);
}

// Legacy code uses V1, new code uses V2

Marker Interfaces and Alternatives

Traditional Marker Interface

// Marker interface - no members, just marks a type
public interface ISerializable { }

public class Document : ISerializable
{
    public string Content { get; set; }
}

// Usage
public void Save(object obj)
{
    if (obj is ISerializable)
    {
        // Serialize and save
    }
}

Modern Alternative: Attributes

// Attribute-based approach
[AttributeUsage(AttributeTargets.Class)]
public class SerializableAttribute : Attribute { }

[Serializable]
public class Document
{
    public string Content { get; set; }
}

// Usage with reflection
public void Save(object obj)
{
    var type = obj.GetType();
    if (type.GetCustomAttribute<SerializableAttribute>() != null)
    {
        // Serialize and save
    }
}

When to Use Each

Use Case Marker Interface Attribute
Compile-time checking βœ… ❌
Generic constraints βœ… ❌
Runtime metadata ❌ βœ…
Multiple markers ⚠️ Works but verbose βœ…
Configuration data ❌ βœ…
// Marker interface enables compile-time constraint
public class Repository<T> where T : IEntity  // Compile-time check
{
    public void Add(T entity) { }
}

// Attribute - only runtime checking
[Table("Products")]
[Index("Name")]
public class Product
{
    [Key]
    public int Id { get; set; }

    [Required]
    [MaxLength(100)]
    public string Name { get; set; }
}

Interview Questions

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

Answer:

Feature Interface Abstract Class
Multiple inheritance Yes No
Fields No (except static in C# 11) Yes
Constructors No Yes
Access modifiers Public only (mostly) Any
Default implementation Yes (C# 8+) Yes
State No Yes

Use interface for: Defining contracts, multiple inheritance, unrelated types sharing behavior Use abstract class for: Sharing code, hierarchy with common state, template method pattern


2. Can you explain default interface methods? When would you use them?

Answer: Default interface methods (C# 8+) allow providing implementation in interfaces.

Use cases:

  1. Versioning: Add new members without breaking implementations
  2. Utility methods: Provide convenience overloads
  3. Trait-like patterns: Compose behaviors
public interface ILogger
{
    void Log(string message);

    // Default implementation
    void LogError(string message) => Log($"ERROR: {message}");
}

Caution: Can’t access instance state (no fields), implementation in interface is a code smell if overused.


3. What is explicit interface implementation and when do you use it?

Answer: Explicit implementation means the member is only accessible through the interface type.

void IDisposable.Dispose() { }  // Only via IDisposable cast

Use when:

  • Hiding rarely-used members
  • Resolving name conflicts between interfaces
  • Implementing infrastructure interfaces (IDisposable) that shouldn’t pollute the public API

4. How do you handle versioning of interfaces?

Answer: Three main approaches:

  1. Default methods (C# 8+): Add with default implementation
  2. Interface segregation: Create new interface for new capability
  3. Versioned interfaces: IRepositoryV2 : IRepositoryV1

Best practice: Design interfaces to be minimal and focused (ISP). Easier to add new interfaces than modify existing ones.


5. Can an abstract class implement an interface partially?

Answer: Yes! An abstract class can implement some interface members and leave others abstract.

public interface IRepository<T>
{
    T GetById(int id);
    void Add(T entity);
    void Delete(int id);
}

public abstract class RepositoryBase<T> : IRepository<T>
{
    // Concrete implementation
    public void Delete(int id)
    {
        // Shared delete logic
    }

    // Abstract - derived class must implement
    public abstract T GetById(int id);
    public abstract void Add(T entity);
}

Key Takeaways

  1. Interfaces define contracts - What can be done, not how
  2. Abstract classes provide base implementation - Share code in hierarchies
  3. Default interface methods - Enable versioning without breaking changes
  4. Multiple interfaces > single abstract class - Better flexibility
  5. Explicit implementation - Hide or resolve conflicts
  6. Marker interfaces - Use attributes for metadata, interfaces for constraints

Further Reading